证据面板的分析结论:结果表的格写列名,总数那一格的值按服务端的写法显示。①「这一处引用的是 结果表第 1 行『share』」原来写的是列 id,现在服务端在 cited 上另给 column_label(结果表记的 labels,如「占变化的比重」),目录条目也带上 labels,证据面板、读屏的出处、导出页、可验证报告都照列名写;② 输入里「总数那一格 … = 159175.18」和同一节的闭合核对「159,175.18」两个写法并排,现在服务端另给 value_rendered(render_cell,千分位、NULL 写「—」,和闭合核对同一个写法),三处界面照原样显示。结果表数的换算(比率、空格)不在这次改动里

This commit is contained in:
ThearchyHelios
2026-10-21 15:53:27 +00:00
parent b919890b7b
commit c54196326f
13 changed files with 171 additions and 44 deletions
+14 -4
View File
@@ -1067,7 +1067,10 @@ def _catalog_alias_of(catalog: dict[str, Any], artifact: Any) -> str | None:
def _analysis_cited(cite: dict[str, Any], result: dict[str, Any] | None) -> dict[str, Any]: def _analysis_cited(cite: dict[str, Any], result: dict[str, Any] | None) -> dict[str, Any]:
"""这一处引用的是结论的哪一项:判定语(phrase)、具名标量(value:键、原值、换算)、结果表的一格(cell:行、列、原值)。""" """这一处引用的是结论的哪一项:判定语(phrase)、具名标量(value:键、原值、换算)、结果表的一格(cell:行、列、原值)。
结果表的列写给人看:column 是稳定的列 id(share、delta……,引用里就是这么写的),column_label 是结果表记的列名
(「占变化的比重」);页面照原样显示 column_label,不把列 id 当叫法。"""
locator = cite.get("locator") or {} locator = cite.get("locator") or {}
if cite.get("kind") == "cell": if cite.get("kind") == "cell":
row, column = locator.get("row"), locator.get("column") row, column = locator.get("row"), locator.get("column")
@@ -1076,7 +1079,9 @@ def _analysis_cited(cite: dict[str, Any], result: dict[str, Any] | None) -> dict
if table is not None: if table is not None:
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
raw, column = locate_cell(table, row, column) raw, column = locate_cell(table, row, column)
return {"what": "cell", "row": row, "column": column, "raw": raw} labels = (table or {}).get("labels") if isinstance((table or {}).get("labels"), dict) else {}
return {"what": "cell", "row": row, "column": column,
"column_label": str(labels.get(column) or column) if column is not None else None, "raw": raw}
if locator.get("key"): if locator.get("key"):
values = (result or {}).get("values") if isinstance((result or {}).get("values"), dict) else {} values = (result or {}).get("values") if isinstance((result or {}).get("values"), dict) else {}
return {"what": "value", "key": locator["key"], "raw": values.get(locator["key"]), return {"what": "value", "key": locator["key"], "raw": values.get(locator["key"]),
@@ -1108,12 +1113,13 @@ async def _analysis_chain(seg: dict[str, Any], cite: dict[str, Any], entry: dict
"""分析结论的出处链(00-总方案 §3.3):判定 → 方法 → 计算 → 输入 → 复算 → 封存。 """分析结论的出处链(00-总方案 §3.3):判定 → 方法 → 计算 → 输入 → 复算 → 封存。
- 判定(analysis):判定语、判定标准、判定码和点名的段、附带说明、算不了的原因,这一处引用的是哪一项(判定语、具名标量、 - 判定(analysis):判定语、判定标准、判定码和点名的段、附带说明、算不了的原因,这一处引用的是哪一项(判定语、具名标量、
结果表的一格);「在验证什么」另列,谁写的照实标:Agent 现场做的写「Agent 写的假设」 结果表的一格,列另写结果表记的列名 column_label);「在验证什么」另列,谁写的照实标:Agent 现场做的写「Agent 写的假设」
(HYPOTHESIS_NOTE),分析卡做的写「分析卡上写的『在验证什么』」(CARD_HYPOTHESIS_NOTE,模板作者写的) (HYPOTHESIS_NOTE),分析卡做的写「分析卡上写的『在验证什么』」(CARD_HYPOTHESIS_NOTE,模板作者写的)
- 方法(analysis_method):方法与版本、规范化后的参数、实际用的阈值(只列这个算子用到的)、现查的表范围(probes) - 方法(analysis_method):方法与版本、规范化后的参数、实际用的阈值(只列这个算子用到的)、现查的表范围(probes)
- 计算(analysis_calc):结果表(前 table_top_k 段,加上被引用的那一行附近)、具名的数、关键数的代入式 - 计算(analysis_calc):结果表(前 table_top_k 段,加上被引用的那一行附近)、具名的数、关键数的代入式
(evidence.analysis_formulas) (evidence.analysis_formulas)
- 输入(analysis_input,每份一步):落到报告目录的哪个 Q、用了哪些列和行、headline 那一格和结论里总数的闭合核对、 - 输入(analysis_input,每份一步):落到报告目录的哪个 Q、用了哪些列和行、headline 那一格(值另由服务端渲染成 value_rendered,
和闭合核对同一个写法)和结论里总数的闭合核对、
下钻的上一层是哪个 A;后面接上每份输入快照的查询步骤(只认封存范围内交回过的,遮罩照查询步骤的办法) 下钻的上一层是哪个 A;后面接上每份输入快照的查询步骤(只认封存范围内交回过的,遮罩照查询步骤的办法)
- 复算(analysis_recompute):按 sha256 取回封存的输入、用同一个函数复算,和封存的逐字段比(evidence.analysis_recheck); - 复算(analysis_recompute):按 sha256 取回封存的输入、用同一个函数复算,和封存的逐字段比(evidence.analysis_recheck);
方法升过版、复算不了都照实写 方法升过版、复算不了都照实写
@@ -1244,6 +1250,10 @@ async def _analysis_inputs(alias: str, result: dict[str, Any], catalog: dict[str
"role_label": roles.get(role, role or "输入"), "artifact": artifact or None, "role_label": roles.get(role, role or "输入"), "artifact": artifact or None,
**{k: item[k] for k in ("columns", "rows", "row", "column", "value") if k in item}, **{k: item[k] for k in ("columns", "rows", "row", "column", "value") if k in item},
} }
# 那一格的值由服务端渲染(render_cell:千分位、NULL 写「—」,和闭合核对里的字同一个写法),页面照原样显示
if "value" in item:
with contextlib.suppress(RenderError):
step["value_rendered"] = render_cell(item.get("value"))
if role == "parent": if role == "parent":
step["alias"] = _catalog_alias_of(catalog, artifact) step["alias"] = _catalog_alias_of(catalog, artifact)
parent, ok = _analysis_of(artifact) parent, ok = _analysis_of(artifact)
+12
View File
@@ -1461,6 +1461,7 @@
const loc = isObj(cite.locator) ? cite.locator : {} const loc = isObj(cite.locator) ? cite.locator : {}
if (cite.kind === 'metric') return T.srcMetric(own(entry, 'name') || loc.metric || cite.ref || '') if (cite.kind === 'metric') return T.srcMetric(own(entry, 'name') || loc.metric || cite.ref || '')
if (cite.kind === 'cell' && own(entry, 'kind') === 'analysis' && Number.isInteger(loc.row)) { if (cite.kind === 'cell' && own(entry, 'kind') === 'analysis' && Number.isInteger(loc.row)) {
// 结果表的格写列名:核对到结论之后换成结论记的列名(checkAnalysis 的 info.source),这里先照引用里的列 id 说
return T.srcAnalysisCell(cite.alias, loc.row, String(loc.column ?? '')) return T.srcAnalysisCell(cite.alias, loc.row, String(loc.column ?? ''))
} }
if (cite.kind === 'cell') { if (cite.kind === 'cell') {
@@ -1573,6 +1574,9 @@
const [raw, column, key] = locateCell(table, loc.row, loc.column) const [raw, column, key] = locateCell(table, loc.row, loc.column)
info.row = loc.row info.row = loc.row
info.column = column info.column = column
// 出处面板写结果表记的列名(labels),不把列 id 当叫法;目录条目和文档引用里带的那份先认,没有再查这份结论
info.columnLabel = analysisColumnLabel(table, column)
info.source = T.srcAnalysisCell(cite.alias, loc.row, info.columnLabel)
info.truth = cellValue(raw, columnKind(table, column)) info.truth = cellValue(raw, columnKind(table, column))
info.truthToken = tokenAt(tokensOf(art), ['table', 'rows', loc.row, key]) info.truthToken = tokenAt(tokensOf(art), ['table', 'rows', loc.row, key])
if (typeof raw === 'string' && typeof info.truth === 'number') info.truthText = pyStrip(raw) if (typeof raw === 'string' && typeof info.truth === 'number') info.truthText = pyStrip(raw)
@@ -1600,6 +1604,14 @@
return checkShown(info, seg, () => renderCell(result.phrase, null, null, 'text', 1)) return checkShown(info, seg, () => renderCell(result.phrase, null, null, 'text', 1))
} }
/** 分析结论结果表的一格写给人看的列名:结论结果表记的 labels(「占变化的比重」),没起名字的列才写列 id。
* 文档里的引用只记列 id(引用就这么写),列名照随文件附带的结论认 */
function analysisColumnLabel(table, column) {
const id = String(column ?? '')
const labels = isObj(table.labels) ? table.labels : {}
return own(labels, id) ? String(labels[id]) : id
}
/** 口径卡的单位:服务端是 str(unit or ""),不是文字的本页不认 */ /** 口径卡的单位:服务端是 str(unit or ""),不是文字的本页不认 */
function unitOf(unit) { function unitOf(unit) {
if (!unit) return '' if (!unit) return ''
+7 -3
View File
@@ -1030,7 +1030,8 @@ _ANALYSIS_KEYS = ("node_id", "exec", "artifact", "call_id", "item", "op", "mode"
def _analysis_entry(entry: dict[str, Any], alias: str, snaps: _Snapshots) -> dict[str, Any]: def _analysis_entry(entry: dict[str, Any], alias: str, snaps: _Snapshots) -> dict[str, Any]:
"""台账里的一条分析结论 → 写作目录的 A 条目:照抄台账的键,带上从工件取回、复验过哈希的整份结论(result, """台账里的一条分析结论 → 写作目录的 A 条目:照抄台账的键,带上从工件取回、复验过哈希的整份结论(result,
analysis.contract.catalog_result 读它)。取不回、哈希不符、形状不对的不带 result,记 unreadable(以编号开头的一句,引用 analysis.contract.catalog_result 读它)。取不回、哈希不符、形状不对的不带 result,记 unreadable(以编号开头的一句,引用
它时照实说)。rows 是结果表的行数:依据 [[see:A1.r3]] 按它核对,存进报告文档的摘要里也留着。""" 它时照实说)。rows 是结果表的行数:依据 [[see:A1.r3]] 按它核对,存进报告文档的摘要里也留着;labels 是结果表记的列名
(列 id → 给人看的名字):证据面板、读屏的出处写「结果表第 1 行「占变化的比重」」,不把列 id 当叫法。"""
out: dict[str, Any] = {"alias": alias, "kind": LEDGER_KIND, "eid": make_eid(LEDGER_KIND, entry.get("artifact"), {}), out: dict[str, Any] = {"alias": alias, "kind": LEDGER_KIND, "eid": make_eid(LEDGER_KIND, entry.get("artifact"), {}),
"locator": {}, **{k: entry[k] for k in _ANALYSIS_KEYS if k in entry}} "locator": {}, **{k: entry[k] for k in _ANALYSIS_KEYS if k in entry}}
result, why = snaps.analysis(alias, {"artifact": entry.get("artifact")}) result, why = snaps.analysis(alias, {"artifact": entry.get("artifact")})
@@ -1039,8 +1040,11 @@ def _analysis_entry(entry: dict[str, Any], alias: str, snaps: _Snapshots) -> dic
else: else:
out["result"] = result out["result"] = result
table = result.get("table") table = result.get("table")
if isinstance(table, dict) and isinstance(table.get("rows"), list): if isinstance(table, dict):
out["rows"] = len(table["rows"]) if isinstance(table.get("rows"), list):
out["rows"] = len(table["rows"])
if isinstance(table.get("labels"), dict) and table["labels"]:
out["labels"] = {str(k): str(v) for k, v in table["labels"].items()}
phrase = _clean(str((result or {}).get("phrase") or "")) phrase = _clean(str((result or {}).get("phrase") or ""))
name = method_label(out.get("op"), out.get("mode")) name = method_label(out.get("op"), out.get("mode"))
out["label"] = f"{name} · {phrase}" if phrase else name out["label"] = f"{name} · {phrase}" if phrase else name
+3 -2
View File
@@ -468,7 +468,8 @@
const verdict = isObj(step.verdict) ? step.verdict : null const verdict = isObj(step.verdict) ? step.verdict : null
const cited = isObj(step.cited) ? step.cited : {} const cited = isObj(step.cited) ? step.cited : {}
const citedText = cited.what === 'value' ? T.citedValue(cited.key) : cited.what === 'cell' const citedText = cited.what === 'value' ? T.citedValue(cited.key) : cited.what === 'cell'
? T.citedCell(Number(cited.row) || 0, String(cited.column ?? '')) : T.citedPhrase // 结果表的格写列名(服务端照结果表记的 labels 给),不把列 id 当叫法;老接口没给列名的照旧写 id
? T.citedCell(Number(cited.row) || 0, String(cited.column_label || cited.column || '')) : T.citedPhrase
body.push(kv([ body.push(kv([
[T.dPhrase, String(step.phrase || NONE)], [T.dPhrase, String(step.phrase || NONE)],
step.criterion ? [T.dCriterion, String(step.criterion)] : null, step.criterion ? [T.dCriterion, String(step.criterion)] : null,
@@ -561,7 +562,7 @@
step.node_id ? [T.dNode, nodeLabel(step.node_id)] : null, step.node_id ? [T.dNode, nodeLabel(step.node_id)] : null,
Array.isArray(step.columns) && step.columns.length ? [T.dColumnsUsed, step.columns.join('、')] : null, Array.isArray(step.columns) && step.columns.length ? [T.dColumnsUsed, step.columns.join('、')] : null,
rows ? [T.dRowsUsed, rows] : null, rows ? [T.dRowsUsed, rows] : null,
'value' in step ? [ratio ? T.dHeadlineRatio : T.dHeadline, `${step.column ? `第 ${Number(step.row) + 1} 行「${step.column}」 = ` : ''}${valueText(step.value)}`] : null, 'value' in step ? [ratio ? T.dHeadlineRatio : T.dHeadline, `${step.column ? `第 ${Number(step.row) + 1} 行「${step.column}」 = ` : ''}${step.value_rendered ?? valueText(step.value)}`] : null,
closure ? [T.dClosure, closed(closure.headline, closure.total)] : null, closure ? [T.dClosure, closed(closure.headline, closure.total)] : null,
step.phrase ? [T.dPhrase, String(step.phrase)] : null, step.phrase ? [T.dPhrase, String(step.phrase)] : null,
])] ])]
+2
View File
@@ -330,6 +330,8 @@ def test_the_doc_keeps_a_summary_and_reloads_the_result_from_the_artifact():
doc = compose_doc("[[a:A1]],华东占 [[a:A1.top_share|pct]]。", catalog, loader=store.load) doc = compose_doc("[[a:A1]],华东占 [[a:A1.top_share|pct]]。", catalog, loader=store.load)
stored = doc["catalog"]["A1"] stored = doc["catalog"]["A1"]
assert "result" not in stored and stored["artifact"] and stored["rows"] == 4 and stored["verdict"] == "driver" assert "result" not in stored and stored["artifact"] and stored["rows"] == 4 and stored["verdict"] == "driver"
# 结果表记的列名(列 id → 给人看的名字)跟着摘要走:证据面板、读屏的出处写「结果表第 1 行「变化」」
assert stored["labels"]["delta"] == "变化"
# 文档里的摘要照样解析得了:结论按工件取回 # 文档里的摘要照样解析得了:结论按工件取回
assert resolve_ref("a:A1", doc["catalog"], loader=store.load)["status"] == "resolved" assert resolve_ref("a:A1", doc["catalog"], loader=store.load)["status"] == "resolved"
assert verify_doc(doc, doc["catalog"], loader=store.load)["ok"] assert verify_doc(doc, doc["catalog"], loader=store.load)["ok"]
+8 -2
View File
@@ -176,7 +176,8 @@ async def test_named_values_and_result_cells_point_at_what_they_cite(client):
cell = (await client.get( cell = (await client.get(
f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'v:A1.r0.delta')}")).json() f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'v:A1.r0.delta')}")).json()
assert cell["chain"][0]["cited"] == {"what": "cell", "row": 0, "column": "delta", "raw": "-120"} assert cell["chain"][0]["cited"] == {"what": "cell", "row": 0, "column": "delta", "column_label": "变化",
"raw": "-120"}
assert cell["chain"][2]["table"]["highlight"] == {"cells": [[0, "delta"]]} assert cell["chain"][2]["table"]["highlight"] == {"cells": [[0, "delta"]]}
assert cell["note"] == "该值由系统从分析结论的结果表中取出、按固定规则显示,未经模型转写" assert cell["note"] == "该值由系统从分析结论的结果表中取出、按固定规则显示,未经模型转写"
@@ -219,7 +220,9 @@ async def test_a_result_cell_rerenders_the_way_it_was_composed(client, monkeypat
body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{segment['id']}")).json() body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{segment['id']}")).json()
judged = body["chain"][0] judged = body["chain"][0]
assert judged["rendered"] == shown and judged["render_ok"] is True assert judged["rendered"] == shown and judged["render_ok"] is True
assert judged["cited"] == {"what": "cell", "row": 0, "column": by, "raw": snapshot["rows"][0][0]} # column_label 是结果表记的列名:维度列照输入快照里的列名(store、slot),不另起叫法
assert judged["cited"] == {"what": "cell", "row": 0, "column": by, "column_label": by,
"raw": snapshot["rows"][0][0]}
# 导出页的数据块是同一条链 # 导出页的数据块是同一条链
data = data_block((await client.get(f"/api/runs/{made['run']}/report.html")).text) data = data_block((await client.get(f"/api/runs/{made['run']}/report.html")).text)
chain = data["chains"][data["refs"][segment["id"]]] chain = data["chains"][data["refs"][segment["id"]]]
@@ -335,6 +338,9 @@ async def test_headline_inputs_show_the_closure(client):
assert inputs[1]["closure"] == {"mode": "additive", "total_key": "target_total", "total": "2,026", assert inputs[1]["closure"] == {"mode": "additive", "total_key": "target_total", "total": "2,026",
"headline": "2,026", "closed": True} "headline": "2,026", "closed": True}
assert [s["role_label"] for s in inputs[:2]] == ["基期总数", "本期总数"] assert [s["role_label"] for s in inputs[:2]] == ["基期总数", "本期总数"]
# 那一格的值服务端渲染好了(render_cell:千分位),和闭合核对里的字同一个写法;页面照原样显示
assert inputs[0]["value"] == "2220" and inputs[0]["value_rendered"] == "2,220"
assert inputs[1]["value"] == "2026" and inputs[1]["value_rendered"] == "2,026"
assert inputs[1]["query"] == "Q2" and inputs[1]["sealed"] is True and inputs[2]["query"] == "Q1" assert inputs[1]["query"] == "Q2" and inputs[1]["sealed"] is True and inputs[2]["query"] == "Q1"
assert next(s for s in body["chain"] if s["step"] == "analysis_recompute")["status"] == "same" assert next(s for s in body["chain"] if s["step"] == "analysis_recompute")["status"] == "same"
+29 -2
View File
@@ -20,6 +20,8 @@ import app.bundle
import app.report_export import app.report_export
from app.main import app as asgi from app.main import app as asgi
from tests.test_analysis_bundle import bundle_of, card_bundle from tests.test_analysis_bundle import bundle_of, card_bundle
from app.core import artifact_store
from tests.test_analysis_evidence import SNAPSHOT, analyzed
from tests.test_analysis_evidence_api import ( from tests.test_analysis_evidence_api import (
LAST_WEEK, SCOPE, TEXT, THIS_WEEK, comparability_card, ratio_run, sealed_run, seg_id, LAST_WEEK, SCOPE, TEXT, THIS_WEEK, comparability_card, ratio_run, sealed_run, seg_id,
) )
@@ -154,7 +156,8 @@ async def test_the_export_page_opens_the_analysis_steps_and_the_scope_numbers():
"按运行当天现算的界 date('now', '-7 days') = 2026-10-14(现查 · 2026-10-21T09:00:00)", "按运行当天现算的界 date('now', '-7 days') = 2026-10-14(现查 · 2026-10-21T09:00:00)",
"orders.refund_date:这一列一个值都没有(现查", "orders.order_date:2025-01-01 至 2026-10-20(剖析"): "orders.refund_date:这一列一个值都没有(现查", "orders.order_date:2025-01-01 至 2026-10-20(剖析"):
assert text in body, text assert text in body, text
assert "结果表第 1 行「delta」" in out[cell]["body"] # 结果表的格写结果表记的列名(labels 的「变化」),不把列 id 当叫法
assert "结果表第 1 行「变化」" in out[cell]["body"]
assert out["scope:A1"]["steps"] == opened["steps"] assert out["scope:A1"]["steps"] == opened["steps"]
@@ -169,12 +172,36 @@ async def test_the_export_page_speaks_of_the_overall_ratio_for_a_ratio_decomposi
out = run_node(RUN_RUNTIME, RUNTIME, {"data": data, "segs": [ out = run_node(RUN_RUNTIME, RUNTIME, {"data": data, "segs": [
{"seg": phrase, "kind": "analysis", "state": "deterministic", "text": made["result"]["phrase"]}]}) {"seg": phrase, "kind": "analysis", "state": "deterministic", "text": made["result"]["phrase"]}]})
body = out[phrase]["body"] body = out[phrase]["body"]
for text in ("基期整体比率", "整体比率那一格", "整体比率 0.035 与各段分子之和 ÷ 分母之和 0.035 闭合", for text in ("基期整体比率", "整体比率那一格", "第 1 行「rate」 = 0.035",
"整体比率 0.035 与各段分子之和 ÷ 分母之和 0.035 闭合",
"整体比率 0.053 与各段分子之和 ÷ 分母之和 0.053 闭合"): "整体比率 0.053 与各段分子之和 ÷ 分母之和 0.053 闭合"):
assert text in body, text assert text in body, text
assert "各段之和 0.0" not in body assert "各段之和 0.0" not in body
async def test_the_export_page_writes_the_headline_cell_the_way_the_server_rendered_it():
"""总数那一格的值由服务端渲染(render_cell:千分位),页面照原样显示:以前照原值写「2220」,同一节里闭合核对写
「2,220」,一处引用里两个写法并排。"""
totals = {"columns": ["gmv_base", "gmv"], "column_types": {"gmv_base": "number", "gmv": "number"},
"rows": [[2220, 2026]], "sql": "SELECT 2220 AS gmv_base, 2026 AS gmv", "source": "shop",
"truncated": False}
t = artifact_store.content_hash(artifact_store.canonical_json(totals))
q = artifact_store.content_hash(artifact_store.canonical_json(SNAPSHOT))
result = await analyzed({"Q2": (SNAPSHOT, q), "Q5": (totals, t)}, headline=["Q5.r0.gmv_base", "Q5.r0.gmv"])
made = await sealed_run("[[a:A1]]。", result=result, queries=[totals])
async with AsyncClient(transport=ASGITransport(app=asgi), base_url="http://test") as client:
html = (await client.get(f"/api/runs/{made['run']}/report.html")).text
data = re.search(r'<script type="application/json" id="report-data">(.*?)</script>', html, re.S).group(1)
phrase = seg_id(made["doc"], "a:A1")
out = run_node(RUN_RUNTIME, RUNTIME, {"data": data, "segs": [
{"seg": phrase, "kind": "analysis", "state": "deterministic", "text": made["result"]["phrase"]}]})
body = out[phrase]["body"]
for text in ("总数那一格第 1 行「gmv_base」 = 2,220", "总数那一格第 1 行「gmv」 = 2,026",
"总数 2,220 与各段之和 2,220 闭合"):
assert text in body, text
assert "= 2220" not in body and "= 2026" not in body
async def test_the_bundle_page_shows_the_analysis_detail_and_the_scope(): async def test_the_bundle_page_shows_the_analysis_detail_and_the_scope():
bundle, _, _ = bundle_of(doc_extra=SCOPE) bundle, _, _ = bundle_of(doc_extra=SCOPE)
out = run_node(RUN_VERIFIER, VERIFIER, {"data": json.dumps(bundle), "seal": bundle["meta"]["manifest_hash"]}) out = run_node(RUN_VERIFIER, VERIFIER, {"data": json.dumps(bundle), "seal": bundle["meta"]["manifest_hash"]})
+5 -2
View File
@@ -262,10 +262,13 @@ export function sourceOf(seg: EvidenceSegment, doc?: Pick<EvidenceDocData, 'cata
if (!cite) return '' if (!cite) return ''
const entry = cite.alias ? doc?.catalog?.[cite.alias] : undefined const entry = cite.alias ? doc?.catalog?.[cite.alias] : undefined
if (cite.kind === 'analysis' || entry?.kind === 'analysis') { if (cite.kind === 'analysis' || entry?.kind === 'analysis') {
// 分析结论:判定语([[a:A1]])、具名的数([[a:A1.top_share]])、结果表的一格([[v:A1.r0.delta]],cite 的种类是 cell) // 分析结论:判定语([[a:A1]])、具名的数([[a:A1.top_share]])、结果表的一格([[v:A1.r0.delta]],cite 的种类是 cell)。
// 结果表的格写结果表记的列名(目录条目上的 labels),不把列 id 当叫法
const E = ANALYSIS_TEXT.evidence const E = ANALYSIS_TEXT.evidence
const loc = cite.locator ?? {} const loc = cite.locator ?? {}
const what = cite.kind === 'cell' ? E.citedCell(Number(loc.row) || 0, String(loc.column ?? '')) const column = String(loc.column ?? '')
const labels = entry && typeof entry.labels === 'object' && entry.labels ? entry.labels as Record<string, string> : {}
const what = cite.kind === 'cell' ? E.citedCell(Number(loc.row) || 0, labels[column] || column)
: loc.key ? E.citedValue(loc.key) : E.citedPhrase : loc.key ? E.citedValue(loc.key) : E.citedPhrase
return [`${E.title} ${cite.alias ?? ''}`.trim(), what].join(' · ') return [`${E.title} ${cite.alias ?? ''}`.trim(), what].join(' · ')
} }
+5 -2
View File
@@ -97,7 +97,8 @@ function JudgedPart({ step, recompute, seal, overall }: {
const verdict = step.verdict && typeof step.verdict === 'object' ? step.verdict : null const verdict = step.verdict && typeof step.verdict === 'object' ? step.verdict : null
const cited = step.cited ?? {} const cited = step.cited ?? {}
const citedText = cited.what === 'value' ? E.citedValue(String(cited.key ?? '')) const citedText = cited.what === 'value' ? E.citedValue(String(cited.key ?? ''))
: cited.what === 'cell' ? E.citedCell(Number(cited.row) || 0, String(cited.column ?? '')) : E.citedPhrase // 结果表的格写列名(服务端照结果表记的 labels 给),不把列 id 当叫法;老接口没给列名的照旧写 id
: cited.what === 'cell' ? E.citedCell(Number(cited.row) || 0, String(cited.column_label || cited.column || '')) : E.citedPhrase
const refused = step.status === 'refused' const refused = step.status === 'refused'
const trusted = !!overall?.sealed && overall.ok !== false const trusted = !!overall?.sealed && overall.ok !== false
const problems = [ const problems = [
@@ -267,7 +268,9 @@ function InputPart({ step, onQuery, trusted }: { step: EvidenceAnalysisStep; onQ
rows ? [E.rowsUsed, rows] : null, rows ? [E.rowsUsed, rows] : null,
'value' in step && !parent ? [ratio ? E.headlineRatio : E.headline, ( 'value' in step && !parent ? [ratio ? E.headlineRatio : E.headline, (
<span key="v" className="tnum"> <span key="v" className="tnum">
{step.column != null && typeof step.row === 'number' ? `${E.cellAt(step.row, step.column)} = ` : ''}{valueText(step.value)} {step.column != null && typeof step.row === 'number' ? `${E.cellAt(step.row, step.column)} = ` : ''}
{/* 那一格的值服务端渲染好了(千分位、NULL 写「—」,和闭合核对同一个写法),照原样显示;老接口没给的按值本身写 */}
{step.value_rendered ?? valueText(step.value)}
</span> </span>
)] : null, )] : null,
closure && closed ? [E.closure, ( closure && closed ? [E.closure, (
@@ -423,7 +423,16 @@
"op": "decompose", "op": "decompose",
"rows": 4, "rows": 4,
"status": "ok", "status": "ok",
"verdict": "driver" "verdict": "driver",
"labels": {
"base": "基期",
"delta": "变化",
"flag": "说明",
"pct": "变化率",
"region": "地区",
"share": "占变化的比重",
"target": "本期"
}
}, },
"A2": { "A2": {
"alias": "A2", "alias": "A2",
@@ -460,7 +469,14 @@
"op": "comparable", "op": "comparable",
"rows": 5, "rows": 5,
"status": "ok", "status": "ok",
"verdict": "comparable" "verdict": "comparable",
"labels": {
"base": "基期",
"check": "检查项",
"note": "说明",
"status": "结果",
"target": "本期"
}
}, },
"Q1": { "Q1": {
"alias": "Q1", "alias": "Q1",
@@ -898,7 +914,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -919,7 +936,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -1301,7 +1319,8 @@
"what": "cell", "what": "cell",
"row": 0, "row": 0,
"column": "delta", "column": "delta",
"raw": "-300" "raw": "-300",
"column_label": "变化"
}, },
"rendered": "-300", "rendered": "-300",
"render_ok": true, "render_ok": true,
@@ -1557,7 +1576,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -1578,7 +1598,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -2212,7 +2233,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -2233,7 +2255,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -3508,7 +3531,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -3529,7 +3553,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -5034,7 +5059,16 @@
"op": "decompose", "op": "decompose",
"rows": 4, "rows": 4,
"status": "ok", "status": "ok",
"verdict": "driver" "verdict": "driver",
"labels": {
"base": "基期",
"delta": "变化",
"flag": "说明",
"pct": "变化率",
"region": "地区",
"share": "占变化的比重",
"target": "本期"
}
}, },
"Q1": { "Q1": {
"alias": "Q1", "alias": "Q1",
@@ -5420,7 +5454,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -5441,7 +5476,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -6063,7 +6099,8 @@
"what": "cell", "what": "cell",
"row": 0, "row": 0,
"column": "delta", "column": "delta",
"raw": "-300" "raw": "-300",
"column_label": "变化"
}, },
"rendered": "-300", "rendered": "-300",
"render_ok": true, "render_ok": true,
@@ -6319,7 +6356,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -6340,7 +6378,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -6722,7 +6761,8 @@
"what": "cell", "what": "cell",
"row": 0, "row": 0,
"column": "delta", "column": "delta",
"raw": "-300" "raw": "-300",
"column_label": "变化"
}, },
"rendered": "-300", "rendered": "-300",
"render_ok": true, "render_ok": true,
@@ -6978,7 +7018,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -6999,7 +7040,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -8158,7 +8200,8 @@
"total": "2,700", "total": "2,700",
"headline": "2,700", "headline": "2,700",
"closed": true "closed": true
} },
"value_rendered": "2,700"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
@@ -8179,7 +8222,8 @@
"total": "2,360", "total": "2,360",
"headline": "2,360", "headline": "2,360",
"closed": true "closed": true
} },
"value_rendered": "2,360"
}, },
{ {
"step": "analysis_input", "step": "analysis_input",
+12 -2
View File
@@ -2435,8 +2435,16 @@ export interface EvidenceAnalysisStep {
artifact?: string | null artifact?: string | null
eid?: string eid?: string
eid_ok?: boolean eid_ok?: boolean
/** 这一处引用的是结论的哪一项:判定语、具名的数(key)、结果表的一格(row、column) */ /** 这一处引用的是结论的哪一项:判定语、具名的数(key)、结果表的一格(row、column;column_label 是结果表的列名,给人看的) */
cited?: { what?: 'phrase' | 'value' | 'cell' | string; key?: string; row?: number; column?: string; raw?: unknown; conv?: string } cited?: {
what?: 'phrase' | 'value' | 'cell' | string
key?: string
row?: number
column?: string
column_label?: string | null
raw?: unknown
conv?: string
}
rendered?: string rendered?: string
render_ok?: boolean render_ok?: boolean
hash_ok?: boolean | null hash_ok?: boolean | null
@@ -2481,6 +2489,8 @@ export interface EvidenceAnalysisStep {
row?: number row?: number
column?: string column?: string
value?: string | null value?: string | null
/** 那一格的值按服务端的规则渲染出来的字(render_cell:千分位、NULL 写「—」),和闭合核对里的写法一致 */
value_rendered?: string
/** 输入落到报告目录的哪个 Q */ /** 输入落到报告目录的哪个 Q */
query?: string | null query?: string | null
sealed?: boolean sealed?: boolean
+2 -1
View File
@@ -940,7 +940,8 @@ await section('样本页:各种形状', async () => {
&& shareDetail.includes('与按显示规则渲染的结果一致'), shareDetail.replace(/\s+/g, ' ').slice(0, 300)) && shareDetail.includes('与按显示规则渲染的结果一致'), shareDetail.replace(/\s+/g, ' ').slice(0, 300))
const cell = page.locator('.doc p button.num').filter({ hasText: /^-120$/ }) const cell = page.locator('.doc p button.num').filter({ hasText: /^-120$/ })
const cellDetail = await cell.count() ? await detailText(page, cell.first()) : '找不到「-120」' const cellDetail = await cell.count() ? await detailText(page, cell.first()) : '找不到「-120」'
check('analysis:结果表的格:出处写分析结论 A1 的结果表 · 第 1 行 · delta', cellDetail.includes('分析结论 A1 的结果表 · 第 1 行 · delta'), // 列写结果表记的列名(「变化」),不把列 id(delta)当叫法
check('analysis:结果表的格:出处写分析结论 A1 的结果表 · 第 1 行 · 变化', cellDetail.includes('分析结论 A1 的结果表 · 第 1 行 · 变化'),
cellDetail.replace(/\s+/g, ' ').slice(0, 300)) cellDetail.replace(/\s+/g, ' ').slice(0, 300))
check('analysis:结果表整表引用:表头是给人看的列名,数字列右对齐', (await page.locator('.doc table th').allTextContents()).join('、') === '地区、基期、本期、变化' check('analysis:结果表整表引用:表头是给人看的列名,数字列右对齐', (await page.locator('.doc table th').allTextContents()).join('、') === '地区、基期、本期、变化'
&& await page.locator('.doc td.n button.num').count() === 12, (await page.locator('.doc table th').allTextContents()).join('、')) && await page.locator('.doc td.n button.num').count() === 12, (await page.locator('.doc table th').allTextContents()).join('、'))
+6 -2
View File
@@ -619,6 +619,9 @@ await section('analysis', '分析结论:判定语、具名的数、结果表
=== JSON.stringify(['headline_base', 'headline_target', 'data'])) === JSON.stringify(['headline_base', 'headline_target', 'data']))
check('……总数那一格和各段之和的闭合核对(服务端算好的数)', (await inputs.nth(0).innerText()).includes('总数 2,700 与各段之和 2,700 闭合') check('……总数那一格和各段之和的闭合核对(服务端算好的数)', (await inputs.nth(0).innerText()).includes('总数 2,700 与各段之和 2,700 闭合')
&& await inputs.nth(0).locator('[data-ev-analysis-closure="closed"]').count() === 1) && await inputs.nth(0).locator('[data-ev-analysis-closure="closed"]').count() === 1)
// 那一格的值也由服务端渲染(千分位),和闭合核对里的数同一个写法:原来照原值写「2700」,同一节里 2,700 和 2700 并排
check('……那一格的值按服务端的写法显示(千分位,和闭合核对一致)', (await inputs.nth(0).innerText()).includes('第 1 行「gmv」 = 2,700')
&& !(await inputs.nth(0).innerText()).includes('= 2700'), await inputs.nth(0).innerText())
check('……数据:用到的列、全部行', (await inputs.nth(2).innerText()).includes('region、week、gmv') && (await inputs.nth(2).innerText()).includes('全部行')) check('……数据:用到的列、全部行', (await inputs.nth(2).innerText()).includes('region、week、gmv') && (await inputs.nth(2).innerText()).includes('全部行'))
check('输入快照的查询步骤接在后面,写明是 A1 的输入', await p.locator('[data-ev-input-of="A1"]').count() === 2) check('输入快照的查询步骤接在后面,写明是 A1 的输入', await p.locator('[data-ev-input-of="A1"]').count() === 2)
await inputs.nth(2).locator('[data-ev-goto-query="Q2"]').click() await inputs.nth(2).locator('[data-ev-goto-query="Q2"]').click()
@@ -634,9 +637,10 @@ await section('analysis', '分析结论:判定语、具名的数、结果表
&& await p.locator('[data-ev-analysis-value="top_share"][data-ev-cited]').count() === 1) && await p.locator('[data-ev-analysis-value="top_share"][data-ev-cited]').count() === 1)
await box.locator('[data-seg="s7"]').click() await box.locator('[data-seg="s7"]').click()
await p.locator('[data-ev-analysis-cited="cell"]').waitFor({ timeout: 8000 }) await p.locator('[data-ev-analysis-cited="cell"]').waitFor({ timeout: 8000 })
check('结果表的格(-300):引用的是结果表第 1 行「delta」,那一格高亮', (await p.locator('[data-ev-analysis-cited]').innerText()) === '结果表第 1 行「delta」' // 列写结果表记的列名(「变化」),不把列 id(delta)当叫法
check('结果表的格(-300):引用的是结果表第 1 行「变化」,那一格高亮', (await p.locator('[data-ev-analysis-cited]').innerText()) === '结果表第 1 行「变化」'
&& await p.locator('[data-ev-analysis-table] td[data-highlight="cell"]').count() === 1) && await p.locator('[data-ev-analysis-table] td[data-highlight="cell"]').count() === 1)
check('……读屏写出处:分析结论 A1 · 结果表第 1 行「delta」', (await box.locator('[data-seg="s7"]').getAttribute('aria-label') ?? '').includes('分析结论 A1 · 结果表第 1 行「delta」')) check('……读屏写出处:分析结论 A1 · 结果表第 1 行「变化」', (await box.locator('[data-seg="s7"]').getAttribute('aria-label') ?? '').includes('分析结论 A1 · 结果表第 1 行「变化」'))
check('片段接口按片段 id 取(s3、s9、s7)', ['seg:s3', 'seg:s9', 'seg:s7'].every((h) => hits.includes(h)), hits.join(',')) check('片段接口按片段 id 取(s3、s9、s7)', ['seg:s3', 'seg:s9', 'seg:s7'].every((h) => hits.includes(h)), hits.join(','))
// 分析卡的结论:谁做的写分析卡(模板定好的分析);表格里引用结果表的格 // 分析卡的结论:谁做的写分析卡(模板定好的分析);表格里引用结果表的格