引用分析结论时没写换算的按结论记的格式渲染(显示规则 4 版):[[v:A1.r0.share]]、[[a:A1.top_share]] 取这一列 / 这个数的格式(占比、变化率按百分数,比率的变化按百分点),写作者自己写了换算的照他的;实际用的换算记在引用上,展开整表时每一格也照写。写作目录的具名的数、结果表的行按同一格式写。3 版及以前组装的文档照当年原样显示,复核按记下的版本,不判对不上。pct、pp 两种换算的写法(不是 0 的不写成 0.0);可验证报告按引用上的换算、4 版起按结论记的格式渲染,不认识的显示规则版本(5 版)判无法核对

This commit is contained in:
ThearchyHelios
2026-10-21 15:55:11 +00:00
parent 3ecc6223df
commit 55376fd332
2 changed files with 109 additions and 18 deletions
+33 -5
View File
@@ -596,7 +596,8 @@
const shifted = (d, k) => ({ neg: d.neg, coef: d.coef, exp: d.exp + k }) const shifted = (d, k) => ({ neg: d.neg, coef: d.coef, exp: d.exp + k })
/** _render_numeric:万 / 亿留两位、去尾零,单位接在后面;pct、比率乘 100 加 %(不接单位);int、.N 改小数位 */ /** _render_numeric:万 / 亿留两位、去尾零,单位接在后面;pct、比率乘 100 加 %(不接单位);pp 乘 100 写成「+1.2 个百分点」;
* int、.N 改小数位 */
function renderNumeric(d, unit, decimals, fmt, conv) { function renderNumeric(d, unit, decimals, fmt, conv) {
if (adjusted(d) >= MAX_DIGITS) throw new Unshown(T.shownTooLong) if (adjusted(d) >= MAX_DIGITS) throw new Unshown(T.shownTooLong)
const ratio = fmt === 'percent_of_ratio' const ratio = fmt === 'percent_of_ratio'
@@ -609,6 +610,20 @@
if (unit.includes('%') || unit.includes('%')) throw new Unshown(T.shownPctUnit) if (unit.includes('%') || unit.includes('%')) throw new Unshown(T.shownPctUnit)
return `${digitsOf(shifted(d, 2), decimals != null ? Math.max(decimals - 2, 0) : 1, group, false)}%` return `${digitsOf(shifted(d, 2), decimals != null ? Math.max(decimals - 2, 0) : 1, group, false)}%`
} }
if (conv === 'pp') {
// 比率的变化按百分点写:一位小数、正数带「+」(0 不带);不是 0 的值写出来成了 0.0 的多给小数位(最多 4 位),
// 不写假的 0(同服务端 engine/evidence._render_numeric 的 pp)
if (unit.includes('%') || unit.includes('%')) throw new Unshown(T.shownPctUnit)
const points = shifted(d, 2)
if (isZero(points)) return `${digitsOf(points, 1, group, false)} 个百分点`
let places = 1
for (; places < 5; places++) {
if (!isZero(quantize(points, -places))) break
}
if (places >= 5) throw new Unshown(T.shownZero)
const out = digitsOf(points, places, group, false)
return `${out.startsWith('-') ? out : `+${out}`} 个百分点`
}
let places let places
if (conv === 'int') places = 0 if (conv === 'int') places = 0
else if (typeof conv === 'string' && CONV_PLACES.test(conv)) places = Number(conv.slice(1)) else if (typeof conv === 'string' && CONV_PLACES.test(conv)) places = Number(conv.slice(1))
@@ -652,12 +667,13 @@
/** /**
* render_cell(shown(raw)):快照里的一格。kind 是查询时记下的列类型,display 是报告文档记下的单元格显示规则版本 * render_cell(shown(raw)):快照里的一格。kind 是查询时记下的列类型,display 是报告文档记下的单元格显示规则版本
* (2 版起 ISO 日期时间的 T 换成空格;3 版起标识列的数不分千位)。plain 是引用上记下的「这一格按标识列显示」: * (2 版起 ISO 日期时间的 T 换成空格;3 版起标识列的数不分千位;4 版起引用分析结论时按结论记的格式换算)。
* plain 是引用上记下的「这一格按标识列显示」:
* 哪几列是标识列服务端按 SQL 和冻结的数据目录判(cell_format.plain_columns),本页解析不了 SQL,照引用上记的渲染—— * 哪几列是标识列服务端按 SQL 和冻结的数据目录判(cell_format.plain_columns),本页解析不了 SQL,照引用上记的渲染——
* 它只决定分不分千位,值本身照样逐位比对。DECIMAL 落成的数字文本按数渲染、不经 float;文字压成一行,竖线换成 ¦ * 它只决定分不分千位,值本身照样逐位比对。DECIMAL 落成的数字文本按数渲染、不经 float;文字压成一行,竖线换成 ¦
*/ */
function renderCell(raw, token, conv, kind, display, plain = false) { function renderCell(raw, token, conv, kind, display, plain = false) {
if (display !== 1 && display !== 2 && display !== 3) throw new Unshown(T.shownDisplay(display)) if (display !== 1 && display !== 2 && display !== 3 && display !== 4) throw new Unshown(T.shownDisplay(display))
if (!PY_NUMERIC || !ISO_DATETIME) throw new Unshown(T.shownOld) if (!PY_NUMERIC || !ISO_DATETIME) throw new Unshown(T.shownOld)
const fmt = display >= 3 && plain === true ? 'plain' : 'thousands' const fmt = display >= 3 && plain === true ? 'plain' : 'thousands'
let value = raw let value = raw
@@ -1546,6 +1562,12 @@
/** 引用上记的换算(|万、|pct 这类),没写是 null */ /** 引用上记的换算(|万、|pct 这类),没写是 null */
const convOf = (cite) => (own(cite, 'conv') === undefined ? null : cite.conv) const convOf = (cite) => (own(cite, 'conv') === undefined ? null : cite.conv)
/** 分析结论给这一列 / 这个数记的换算(formats / value_formats:占比、变化率的列 pct,比率的变化 pp),没记是 null */
const namedConv = (formats, key) => {
const found = isObj(formats) ? formats[key] : null
return typeof found === 'string' && found ? found : null
}
/** /**
* 分析结论的一处引用(checkNumber 已核过工件的哈希和引用链):引用的值等于结论里记的那个值,正文显示的字等于按服务端 * 分析结论的一处引用(checkNumber 已核过工件的哈希和引用链):引用的值等于结论里记的那个值,正文显示的字等于按服务端
* 的规则渲染出来的字。判定语按文本格渲染(engine/evidence.render_cell 的 kind=text:压成一行、竖线换成 ¦),具名的数按 * 的规则渲染出来的字。判定语按文本格渲染(engine/evidence.render_cell 的 kind=text:压成一行、竖线换成 ¦),具名的数按
@@ -1573,7 +1595,10 @@
} }
if (!sameValue(cite.value, info.truth)) return { ...info, result: FAIL, reason: T.numAnalysisCell } if (!sameValue(cite.value, info.truth)) return { ...info, result: FAIL, reason: T.numAnalysisCell }
const display = Object.prototype.hasOwnProperty.call(doc, 'cell_display') ? doc.cell_display : 1 const display = Object.prototype.hasOwnProperty.call(doc, 'cell_display') ? doc.cell_display : 1
return checkShown(info, seg, () => renderCell(cell.raw, info.truthToken, convOf(cite), // 没写换算、结论的结果表给这一列记了格式(formats:4 版显示规则起)、文档也按 4 版复核的,按它渲染(服务端同样);
// 写作者自己写了换算的照他的
const conv = convOf(cite) ?? (display >= 4 ? namedConv(table.formats, cell.column) : null)
return checkShown(info, seg, () => renderCell(cell.raw, info.truthToken, conv,
columnKind(table, cell.column), display, own(cite, 'plain') === true)) columnKind(table, cell.column), display, own(cite, 'plain') === true))
} }
if (loc.key != null) { if (loc.key != null) {
@@ -1584,7 +1609,10 @@
info.truthToken = tokenAt(tokensOf(art), ['values', loc.key]) info.truthToken = tokenAt(tokensOf(art), ['values', loc.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)
if (!sameValue(cite.value, info.truth)) return { ...info, result: FAIL, reason: T.numValueDiffers } if (!sameValue(cite.value, info.truth)) return { ...info, result: FAIL, reason: T.numValueDiffers }
return checkShown(info, seg, () => renderCell(raw, info.truthToken, convOf(cite), null, 1)) // 没写换算、结论给这个数记了格式(value_formats;4 版显示规则起)的按它渲染(服务端同样);写作者自己写了换算的照他的
const display = Object.prototype.hasOwnProperty.call(doc, 'cell_display') ? doc.cell_display : 1
const conv = convOf(cite) ?? (display >= 4 ? namedConv(result.value_formats, loc.key) : null)
return checkShown(info, seg, () => renderCell(raw, info.truthToken, conv, null, 1))
} }
if (typeof result.phrase !== 'string' || !result.phrase.trim()) return { ...info, result: FAIL, reason: T.numNoPhrase } if (typeof result.phrase !== 'string' || !result.phrase.trim()) return { ...info, result: FAIL, reason: T.numNoPhrase }
info.truth = result.phrase info.truth = result.phrase
+76 -13
View File
@@ -91,11 +91,15 @@ ENTITY_SYNTAXES = frozenset({1, 2})
#: 「没有第 N 行」重新解析得了,会被当成文档被改过(render_mismatch)。另外标识列(编码、编号、分组键,见 #: 「没有第 N 行」重新解析得了,会被当成文档被改过(render_mismatch)。另外标识列(编码、编号、分组键,见
#: cell_format.plain_columns)的数不分千位:编码 1203 不写成「1,203」。真有格子因此改了写法的文档记 cell_display: 3, #: cell_format.plain_columns)的数不分千位:编码 1203 不写成「1,203」。真有格子因此改了写法的文档记 cell_display: 3,
#: 引用上记 plain: true(离线核对页解析不了 SQL,照它渲染) #: 引用上记 plain: true(离线核对页解析不了 SQL,照它渲染)
#: - 4:引用分析结论的结果表一格、具名的数时没写换算的,按结论给这一列 / 这个数记的格式渲染(占比、变化率按百分数,
#: 比率的变化按百分点:0.62 显示成 62.0%,不再照原样写 0.618556701),实际用的换算记在引用上(conv,导出页和可验证报告
#: 照它渲染)。3 版及以前组装的文档里的引用照当年原样显示,复核按记下的版本,不因此判对不上
#: 新组装的文档里真有格子因为显示规则改了写法时才记 cell_display(参与内容哈希),记的是渲染得出这些格子的最低一版 #: 新组装的文档里真有格子因为显示规则改了写法时才记 cell_display(参与内容哈希),记的是渲染得出这些格子的最低一版
#: (只有日期时间被改写的记 2:标识列没有因此改写法的,3 版的整表截行不改格子的写法),没有的文档和以前一字不差。复核必须按 #: (只有日期时间被改写的记 2:标识列没有因此改写法的,3 版的整表截行不改格子的写法,引用分析结论时本来就有换算的记 1);没有的
#: 文档生成时的那一版:老文档存的是带 T 的字,拿新规则重新渲染对不上,会被当成文档被改过(render_mismatch) #: 文档和以前一字不差。复核必须按文档生成时的那一版:老文档存的是带 T 的字,拿新规则重新渲染对不上,会被当成文档被改过
CELL_DISPLAY = 3 #: (render_mismatch)
CELL_DISPLAYS = frozenset({1, 2, 3}) CELL_DISPLAY = 4
CELL_DISPLAYS = frozenset({1, 2, 3, 4})
_ISO_DATETIME = re.compile(r"^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)$") _ISO_DATETIME = re.compile(r"^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)$")
@@ -148,7 +152,7 @@ class Citation(TypedDict, total=False):
status: Literal["resolved", "unresolved", "mismatch"] status: Literal["resolved", "unresolved", "mismatch"]
reason: str # unresolved 时为什么 reason: str # unresolved 时为什么
unknown: bool # 实体:本次运行哪里都没有这个名字(可疑实体,不是写法错了) unknown: bool # 实体:本次运行哪里都没有这个名字(可疑实体,不是写法错了)
conv: str # 换算:万 / 亿 / pct / int / .N conv: str # 换算:万 / 亿 / pct / pp / int / .N(没写换算、按分析结论记的格式渲染的也照记,导出、可验证报告照它渲染)
value: Any value: Any
rendered: str rendered: str
source: dict[str, Any] # 引文:原文所在的检索快照、文档、片段 source: dict[str, Any] # 引文:原文所在的检索快照、文档、片段
@@ -573,6 +577,11 @@ MISSING = "—"
_CONV_SCALE = {"万": Decimal(10) ** 4, "亿": Decimal(10) ** 8} _CONV_SCALE = {"万": Decimal(10) ** 4, "亿": Decimal(10) ** 8}
_CONV_PLACES = re.compile(r"^\.([0-6])$") _CONV_PLACES = re.compile(r"^\.([0-6])$")
#: 比率的变化按百分点写(0.012 →「+1.2 个百分点」,-0.003 →「-0.3 个百分点」):分析结论的结果表、具名的数里的换算,
#: 算子记成 pp(analysis.contract.CONV_POINTS)。正数带「+」(和 pct 一样),不是 0 的值写出来成了 0.0 的多给小数位
#: (最多 4 位),不写假的 0
_CONV_POINTS = "pp"
_POINTS_SUFFIX = " 个百分点"
#: 小数位最多写到多少。负数是取整到十位、百位……(round(x, -2) 的意思) #: 小数位最多写到多少。负数是取整到十位、百位……(round(x, -2) 的意思)
MAX_PLACES = 100 MAX_PLACES = 100
#: 整数部分最多多少位。浮点数到头也就 309 位,再大的整数按规则一位位写出来没有意义 #: 整数部分最多多少位。浮点数到头也就 309 位,再大的整数按规则一位位写出来没有意义
@@ -648,7 +657,7 @@ def render_number(
- decimals:固定小数位;不写就按值本来的样子(整数不带小数点) - decimals:固定小数位;不写就按值本来的样子(整数不带小数点)
- 单位直接接在数字后面:45,678.5元、8.7% - 单位直接接在数字后面:45,678.5元、8.7%
- conv 是报告里写的换算:万 / 亿(保留两位、去尾零,单位接在后面:4.57万元)、 - conv 是报告里写的换算:万 / 亿(保留两位、去尾零,单位接在后面:4.57万元)、
pct(比率 ×100 加 %)、int(取整)、.N(N 位小数) pct(比率 ×100 加 %)、pp(比率的变化 ×100 写成「+1.2 个百分点」)、int(取整)、.N(N 位小数)
值是 None 显示「—」。文本值原样(压成一行),不能换算。 值是 None 显示「—」。文本值原样(压成一行),不能换算。
@@ -696,13 +705,30 @@ def _render_numeric(d: Decimal, *, unit: str, decimals: int | None, fmt: str, co
raise RenderError("该指标本身是百分数(单位为 %),不能再换算为 pct") raise RenderError("该指标本身是百分数(单位为 %),不能再换算为 pct")
places = max(decimals - 2, 0) if decimals is not None else 1 places = max(decimals - 2, 0) if decimals is not None else 1
return _digits(_ctx(d).multiply(d, 100), places, group=group, zero=rounded) + "%" return _digits(_ctx(d).multiply(d, 100), places, group=group, zero=rounded) + "%"
if conv == _CONV_POINTS:
# 比率的变化按百分点写:一位小数、正数带「+」(0 不带);不是 0 的值写出来成了 0.0 的多给小数位(最多 4 位),
# 不写假的 0。说法照 phrases.points / ratio_change 里已有的百分点写法,只是表格里的数没有「上升 / 下降」可依,
# 正负靠符号
if "%" in unit or "%" in unit:
raise RenderError("该指标本身是百分数(单位为 %),不能再换算为 pp")
points = _ctx(d).multiply(d, 100)
if points.is_zero():
return _digits(points, 1, group=group) + _POINTS_SUFFIX
zero = f"{shown} 按 |pp 显示为 0,精度不够:请去掉换算,或改用 |.N 多保留几位小数"
for places in range(1, 5):
exp = -(places)
if points.quantize(Decimal(1).scaleb(exp), context=_ctx(points, places)).is_zero():
continue
out = _digits(points, places, group=group, zero=zero)
return ("+" + out if not out.startswith("-") else out) + _POINTS_SUFFIX
raise RenderError(zero)
if conv == "int": if conv == "int":
places: int | None = 0 places: int | None = 0
elif conv and (m := _CONV_PLACES.match(conv)): elif conv and (m := _CONV_PLACES.match(conv)):
places = int(m.group(1)) places = int(m.group(1))
elif conv: elif conv:
raise RenderError(f"无法识别的换算 |{conv},只支持 万 / 亿 / pct / int / .N(N 为 0–6)") raise RenderError(f"无法识别的换算 |{conv},只支持 万 / 亿 / pct / pp / int / .N(N 为 0–6)")
else: else:
rounded = (f"{shown} 按口径卡的小数位({decimals})显示为 0,精度不够:请在口径卡中增加小数位" rounded = (f"{shown} 按口径卡的小数位({decimals})显示为 0,精度不够:请在口径卡中增加小数位"
if decimals is not None else if decimals is not None else
@@ -1464,8 +1490,8 @@ class _Snapshots:
def table(self, alias: str, entry: dict[str, Any]) -> tuple[Any, str | None]: def table(self, alias: str, entry: dict[str, Any]) -> tuple[Any, str | None]:
"""按单元格、整表取的那张表:(表, 取不到的原因)。查询结果是查询快照本身;分析结论是它的结果表(形状同查询快照: """按单元格、整表取的那张表:(表, 取不到的原因)。查询结果是查询快照本身;分析结论是它的结果表(形状同查询快照:
columns 是稳定的列 id,另带 labels 给人看的列名,整表的表头写它)。结果表另记了列类型(column_types)的照着渲染, columns 是稳定的列 id,另带 labels 给人看的列名,整表的表头写它)。结果表另记了列类型(column_types)、各列的换算
没记的按值认(同老快照)。""" (formats,占比、变化率这类以比率存的列)的照着渲染,没记的按值认(同老快照)。"""
if entry.get("kind") != LEDGER_KIND: if entry.get("kind") != LEDGER_KIND:
return self.get(alias, entry.get("artifact")) return self.get(alias, entry.get("artifact"))
result, why = self.analysis(alias, entry) result, why = self.analysis(alias, entry)
@@ -1476,9 +1502,11 @@ class _Snapshots:
return None, f"{alias} 没有结果表" + ("(这次分析没有做成)" if result.get("status") == "refused" else "") return None, f"{alias} 没有结果表" + ("(这次分析没有做成)" if result.get("status") == "refused" else "")
labels = table.get("labels") labels = table.get("labels")
types = table.get("column_types") types = table.get("column_types")
formats = table.get("formats")
return {"columns": [str(c) for c in table.get("columns") or []], "rows": table["rows"], return {"columns": [str(c) for c in table.get("columns") or []], "rows": table["rows"],
"labels": {str(k): str(v) for k, v in labels.items()} if isinstance(labels, dict) else {}, "labels": {str(k): str(v) for k, v in labels.items()} if isinstance(labels, dict) else {},
**({"column_types": types} if isinstance(types, dict) else {})}, None **({"column_types": types} if isinstance(types, dict) else {}),
**({"formats": {str(k): str(v) for k, v in formats.items()}} if isinstance(formats, dict) else {})}, None
def hits(self, alias: str, artifact: Any) -> tuple[list[Any], str | None]: def hits(self, alias: str, artifact: Any) -> tuple[list[Any], str | None]:
"""检索快照里的命中:(命中列表, 取不到的原因)。""" """检索快照里的命中:(命中列表, 取不到的原因)。"""
@@ -1565,6 +1593,19 @@ def _noun(entry: dict[str, Any] | None) -> str:
return "结果表" if (entry or {}).get("kind") == LEDGER_KIND else "查询结果" return "结果表" if (entry or {}).get("kind") == LEDGER_KIND else "查询结果"
def _column_conv(snapshot: Any, column: str, display: int = CELL_DISPLAY) -> str | None:
"""分析结论的结果表给这一列记的换算(ResultTable.formats:占比、变化率的列记 pct,比率的变化记 pp)。查询快照没有这一项,
一切照原样(None)。按分析结论记的格式显示是 4 版起的显示规则:老文档(3 版及以前)里的引用照当年原样显示,复核不因此
判对不上。"""
if display < 4:
return None
formats = snapshot.get("formats") if isinstance(snapshot, dict) else None
if not isinstance(formats, dict):
return None
found = formats.get(column)
return found if isinstance(found, str) and found else None
def _resolve_cell(marker: dict[str, Any], catalog: dict[str, Any], snaps: _Snapshots, def _resolve_cell(marker: dict[str, Any], catalog: dict[str, Any], snaps: _Snapshots,
cells_allowed: bool) -> dict[str, Any]: cells_allowed: bool) -> dict[str, Any]:
ref = marker.get("ref") or "" ref = marker.get("ref") or ""
@@ -1592,7 +1633,14 @@ def _resolve_cell(marker: dict[str, Any], catalog: dict[str, Any], snaps: _Snaps
raw, column = locate_cell(snapshot, parsed["row"], parsed["column"]) raw, column = locate_cell(snapshot, parsed["row"], parsed["column"])
kind = column_kind(snapshot, column) kind = column_kind(snapshot, column)
shown = snaps.shown(raw) shown = snaps.shown(raw)
# 分析结论的结果表记了这一列的换算(占比、变化率这类以比率存的列;4 版显示规则起)、写作者没写换算的,按它渲染;
# 写作者自己写了换算的照他的。实际用的换算记进引用(导出页、可验证报告照它渲染)
conv = conv or _column_conv(snapshot, column, snaps.display)
rendered = render_cell(shown, conv, kind=kind) rendered = render_cell(shown, conv, kind=kind)
if conv and not marker.get("conv") and rendered != render_cell(shown, None, kind=kind):
# 按分析结论记的格式渲染、真改了写法(4 版起):文档的 cell_display 记成 4,引用上记 conv,
# 导出页和可验证报告照引用上的换算渲染
snaps.needed = max(snaps.needed, 4)
plain = column in snaps.plain(entry.get("artifact"), snapshot) \ plain = column in snaps.plain(entry.get("artifact"), snapshot) \
and render_cell(shown, conv, kind=kind, plain=True) != rendered and render_cell(shown, conv, kind=kind, plain=True) != rendered
if plain: if plain:
@@ -1836,6 +1884,7 @@ def _resolve_analysis(marker: dict[str, Any], catalog: dict[str, Any], snaps: _S
cells_allowed: bool) -> dict[str, Any]: cells_allowed: bool) -> dict[str, Any]:
"""[[a:A3]] → 判定语:分析结论里系统按模板渲染好的那一整句(判定是平台的算子算的,写作者只引用,00-总方案 D5)。 """[[a:A3]] → 判定语:分析结论里系统按模板渲染好的那一整句(判定是平台的算子算的,写作者只引用,00-总方案 D5)。
[[a:A3.top_share|pct]] → 具名标量:结论 values 里的一项(数一律十进制字符串),按单元格的规则渲染,换算同 [[m:]]。 [[a:A3.top_share|pct]] → 具名标量:结论 values 里的一项(数一律十进制字符串),按单元格的规则渲染,换算同 [[m:]]。
没写换算、结论给这个数记了格式(value_formats)的按它渲染(0.62 显示成 62.0%,不是 0.62),实际用的换算记进引用。
判定语是一个片段(种类 analysis),里面的数和字都是系统写的:组装、复核都按带引用的片段整段涂白,不算裸数字。受管级别的 判定语是一个片段(种类 analysis),里面的数和字都是系统写的:组装、复核都按带引用的片段整段涂白,不算裸数字。受管级别的
正式出具、契约没开「单元格引用」时,Agent 现场做的分析解析不了(ANALYSIS_CELLS_REASON),分析卡做的照常解析。""" 正式出具、契约没开「单元格引用」时,Agent 现场做的分析解析不了(ANALYSIS_CELLS_REASON),分析卡做的照常解析。"""
@@ -1875,6 +1924,13 @@ def _resolve_analysis(marker: dict[str, Any], catalog: dict[str, Any], snaps: _S
return _unresolved(marker, alias, LEDGER_KIND, _Reason( return _unresolved(marker, alias, LEDGER_KIND, _Reason(
f"{alias} 中没有名为「{key}」的数", f"{alias} 里没有「{key}」,能引用的数:{names}(写成 [[{MARKER_KIND}:{alias}.键]])")) f"{alias} 中没有名为「{key}」的数", f"{alias} 里没有「{key}」,能引用的数:{names}(写成 [[{MARKER_KIND}:{alias}.键]])"))
raw = values[key] raw = values[key]
# 结论里给这个数记了换算(value_formats:占比、变化率这类以比率存的数;4 版显示规则起)而写作者没写换算的,按它渲染;
# 写作者自己写了换算的照他的。实际用的换算记进引用(导出页、可验证报告照它渲染)
named = result.get("value_formats") if isinstance(result.get("value_formats"), dict) else {}
if snaps.display >= 4 and not conv and isinstance(named.get(key), str) and named.get(key):
conv = named[key]
# 按分析结论记的格式渲染了(4 版起):文档记成 4,引用上记 conv(写了换算才真有换算,写法一定和以前不同)
snaps.needed = max(snaps.needed, 4)
try: try:
rendered = render_cell(raw, conv) rendered = render_cell(raw, conv)
except RenderError as e: except RenderError as e:
@@ -4028,7 +4084,8 @@ def _query_prompt(entry: dict[str, Any], snaps: _Snapshots, cells_allowed: bool,
ANALYSIS_PROMPT = ( ANALYSIS_PROMPT = (
"分析结论(系统按平台的方法算好的判定。判定语、具名的数、结果表都只能用引用标记写,系统照结论渲染、逐字核对):\n" "分析结论(系统按平台的方法算好的判定。判定语、具名的数、结果表都只能用引用标记写,系统照结论渲染、逐字核对):\n"
"写法:判定语单独写 [[a:A1]],系统换成整句判定;具名的数写 [[a:A1.键]],换算同指标,比重、变化率这类比率写成 " "写法:判定语单独写 [[a:A1]],系统换成整句判定;具名的数写 [[a:A1.键]],换算同指标,比重、变化率这类比率写成 "
"[[a:A1.top_share|pct]] 显示成百分数;结果表的一格写 [[v:A1.r0.列]](行号从 0 数),要列出多行多列,单独一行写 " "[[a:A1.top_share|pct]] 显示成百分数;比重、变化率不写换算也按百分数显示,比率的变化按百分点([[v:A1.r0.mix]] 显示成「1.2 "
"个百分点」);结果表的一格写 [[v:A1.r0.列]](行号从 0 数),要列出多行多列,单独一行写 "
"[[table:A1 cols=列a,列b rows=0-4]];拿它当结论的依据,句末写 [[see:A1]]。" "[[table:A1 cols=列a,列b rows=0-4]];拿它当结论的依据,句末写 [[see:A1]]。"
"「主要来自」「超出常规波动」「呈上升趋势」这类判断已经写在判定语里:引用判定语,不要自己改写判定;" "「主要来自」「超出常规波动」「呈上升趋势」这类判断已经写在判定语里:引用判定语,不要自己改写判定;"
"算不了的照实写算不了的原因。下面没有的编号、键、列不要编造。" "算不了的照实写算不了的原因。下面没有的编号、键、列不要编造。"
@@ -4101,10 +4158,13 @@ def _analysis_prompt(entry: dict[str, Any], catalog: dict[str, Any], snaps: _Sna
if see_only: if see_only:
return lines return lines
values = result.get("values") if isinstance(result.get("values"), dict) else {} values = result.get("values") if isinstance(result.get("values"), dict) else {}
named = result.get("value_formats") if isinstance(result.get("value_formats"), dict) else {}
shown = [] shown = []
for key, raw in list(values.items())[:_PROMPT_VALUES]: for key, raw in list(values.items())[:_PROMPT_VALUES]:
try: try:
shown.append(f"{key}={render_cell(raw)}") # 具名的数按结论记的格式写(占比、变化率是百分数):写作者在这里看到的就是引用之后显示的字
conv = named.get(key) if isinstance(named.get(key), str) and named.get(key) else None
shown.append(f"{key}={render_cell(raw, conv)}")
except RenderError: except RenderError:
shown.append(f"{key}={MISSING}") shown.append(f"{key}={MISSING}")
if shown: if shown:
@@ -4127,12 +4187,15 @@ def _analysis_prompt(entry: dict[str, Any], catalog: dict[str, Any], snaps: _Sna
top = top if isinstance(top, int) and not isinstance(top, bool) and top > 0 else _TOP_K top = top if isinstance(top, int) and not isinstance(top, bool) and top > 0 else _TOP_K
lines.append(f" 结果表(共 {len(rows)} 段):列 {named}") lines.append(f" 结果表(共 {len(rows)} 段):列 {named}")
plain = snaps.plain(entry.get("artifact"), snapshot) plain = snaps.plain(entry.get("artifact"), snapshot)
formats = snapshot.get("formats") if isinstance(snapshot.get("formats"), dict) else {}
for r in range(min(top, len(rows))): for r in range(min(top, len(rows))):
cells = [] cells = []
for c in columns: for c in columns:
try: try:
raw, _ = locate_cell(snapshot, r, c) raw, _ = locate_cell(snapshot, r, c)
cells.append(f"{c}={render_cell(snaps.shown(raw), kind=column_kind(snapshot, c), plain=c in plain)}") conv = formats.get(c) if isinstance(formats.get(c), str) and formats.get(c) else None
cells.append(f"{c}={render_cell(snaps.shown(raw), conv, kind=column_kind(snapshot, c),
plain=c in plain)}")
except (CellError, RenderError): except (CellError, RenderError):
cells.append(f"{c}={MISSING}") cells.append(f"{c}={MISSING}")
lines.append(f" r{r}:" + ",".join(cells)) lines.append(f" r{r}:" + ",".join(cells))