可验证报告样本页加异动周报(analysis,分析能力一轮):Agent 在虚构零售库 shop 上查两周的总数和按地区两周的销售额,做按地区的变化拆解(带总数核对)、波动基线(两周历史不够,算不了)、两期可比性检查,结论按快照真算(app.analysis.run,目录事实按虚构的数据目录给),Agent 节点的 tool.end 照分析工具真实的写法带结论工件和判定那一段;报告引用拆解的判定语、具名的数、结果表的格和整张结果表,正文后面是 engine/analysis_scope 按虚构的数据目录算出的分析范围(没拆的是渠道)。check-bundle:样本页 19 份;异动周报原样导出全部通过——③ 三份结论和两份输入快照随文件附带,④ 输入快照按「已核对的分析结论的输入」认,⑤ 分开数判定语,点开判定语看得到判定标准、方法与版本、附带说明、Agent 写的假设、输入和导出时的复算,具名的数、结果表的格写对出处,分析范围的编号点得开(正文没引用的 A2 也随文件附带)、范围里的编号不核正文显示,390 宽不溢出;只改判定语、具名的数的显示并重算全部哈希各自变红并点名。另:「查询快照以外的工件标成遮罩」的原因照第 1 波改过的说法(只有查询快照、口径卡、分析结论和报告文档……),原来的断言没跟着改。test_bundle_build 的样本页清单加上它、核对它的形状

This commit is contained in:
ThearchyHelios
2026-10-21 19:09:21 +08:00
parent 019676670b
commit 017cbee5aa
3 changed files with 216 additions and 4 deletions
+124
View File
@@ -38,6 +38,10 @@
原样导出照样全部通过 原样导出照样全部通过
- prose:同经营月报的口径卡和流水,正文是写作者自己写在引用旁边的普通文字(「运费统一[[m:…]]」「合计——[[m:…]]」 - prose:同经营月报的口径卡和流水,正文是写作者自己写在引用旁边的普通文字(「运费统一[[m:…]]」「合计——[[m:…]]」
「SKU-[[v:…]]」「共[[m:…]]家」这类):原样导出全部通过、总览是绿的,紧挨着核对的规则不误判正常写法 「SKU-[[v:…]]」「共[[m:…]]家」这类):原样导出全部通过、总览是绿的,紧挨着核对的规则不误判正常写法
- analysis:异动周报(分析能力一轮)。Agent 在虚构零售库 shop 上查了两周的总数(Q1)和按地区两周的销售额(Q2),做了按地区的
变化拆解(A1,带总数核对)、波动基线(A2,两周历史不够,算不了)、两期可比性检查(A3),结论都是 app.analysis 真算的;
报告引用 A1 的判定语、具名的数(占比)、结果表的格,正文后面是系统写的「分析范围」(engine/analysis_scope 按虚构的数据
目录算出没拆的渠道)。结论和输入快照随文件附带、导出时复算过,原样导出全部通过;分析范围里的编号点得开(A2、A3 正文没引用)
""" """
from __future__ import annotations from __future__ import annotations
@@ -313,6 +317,125 @@ def formats_bundle(*, card: dict[str, Any] = FORMATS_CARD, table: dict[str, Any]
return build(f, events, bound) return build(f, events, bound)
#: 分析样本(analysis)的两份查询:虚构零售库 shop 两周的总数(Q1)、四个地区两周的销售额(Q2)
AN_TOTALS = {"columns": ["week", "gmv"], "column_types": {"week": "text", "gmv": "number"},
"rows": [["2026-W41", 2220], ["2026-W42", 2026]], "source": "shop",
"sql": "SELECT week, SUM(amount) AS gmv FROM orders GROUP BY week ORDER BY week", "truncated": False,
"row_count": 2}
AN_REGIONS = {"columns": ["region", "week", "gmv"], "column_types": {"region": "text", "week": "text", "gmv": "number"},
"rows": [["华东", "2026-W41", 820], ["华东", "2026-W42", 700], ["华北", "2026-W41", 600],
["华北", "2026-W42", 560], ["华南", "2026-W41", 700], ["华南", "2026-W42", 666],
["西南", "2026-W41", 100], ["西南", "2026-W42", 100]], "source": "shop",
"sql": "SELECT region, week, SUM(amount) AS gmv FROM orders GROUP BY region, week ORDER BY region, week",
"truncated": False, "row_count": 8}
#: Agent 的三次分析调用(工具名、请求),照分析工具交给 app.analysis.run 的写法
AN_CALLS = [
("analysis_decompose", {"op": "decompose", "mode": "additive", "hypothesis": "下降是不是集中在某个地区",
"params": {"query": "Q2", "by": ["region"], "period": "week", "base": "2026-W41",
"target": "2026-W42", "measure": "gmv", "label": "销售额",
"headline": {"base": "Q1.r0.gmv", "target": "Q1.r1.gmv"}}}),
("analysis_baseline", {"op": "baseline", "mode": None, "hypothesis": "这周的下降算不算异常",
"params": {"query": "Q1", "period": "week", "measure": "gmv", "label": "销售额"}}),
("analysis_comparable", {"op": "comparable", "mode": None, "hypothesis": "两周能不能直接比",
"params": {"query": "Q2", "period": "week", "base": "2026-W41", "target": "2026-W42",
"measure": "gmv"}}),
]
#: 异动周报:引用 A1 的判定语、具名的数(占比)、结果表的格;A2、A3 只在系统写的分析范围里
AN_REPORT = ("## 异动周报\n\n[[a:A1]]。华东占总变化的 [[a:A1.top_share|pct]],华东的变化是 [[v:A1.r0.delta]]。[[see:A1]]\n\n"
"各地区的变化见下表。\n\n[[table:A1 cols=region,base,target,delta rows=0-3]]")
def _shop_notes() -> dict[str, Any]:
"""虚构零售库 shop 的数据目录(orders 是事实表;地区、渠道是属性列,码值表列全):分析范围按它算没拆的候选维度。"""
from app.data.catalog import make_item
def column(label: str, measure: str, codes: tuple[str, ...] = ()) -> dict[str, Any]:
out = {"label": make_item(label, "human"), "measure": make_item(measure, "human")}
if codes:
out["codes"] = make_item({c: "" for c in codes}, "profile", "verified")
return out
return {"orders": {"kind": make_item("fact", "human"), "columns": {
"region": column("地区", "attribute", ("华东", "华北", "华南", "西南")),
"channel": column("渠道", "attribute", ("线上", "线下")),
"week": column("周", "attribute"), "amount": column("实收金额", "flow")}}}
def analysis_bundle() -> dict[str, Any]:
"""异动周报(analysis)的页面数据:结论按两份快照真算(app.analysis.run),Agent 节点的事件照分析工具真实的写法
(tool.end 带结论工件和判定那一段),报告文档按写作目录真实组装,分析范围按虚构的数据目录真实算出。"""
import asyncio
from datetime import date
from app import analysis
from app.analysis.contract import effective_settings, ledger_entry
from app.engine import analysis_scope
from app.tools.analysis import event_view, input_snapshot
run = {**RUN, "id": "9c2e4a6b8d0f4e1a3c5b7d9f1e3a5c7b", "workflow_id": "wf-sample-anomaly", "workflow_name": "异动周报",
"started_at": "2026-10-19T01:00:00+00:00", "finished_at": "2026-10-19T01:01:30+00:00"}
graph = {"nodes": [{"id": n, "type": t, "data": {"label": label}} for n, t, label in [
("start", "input", "开始"), ("bot", "agent", "问数据"), ("write", "report", "写周报"), ("out", "output", "成果")]]}
files: dict[str, bytes] = {}
def put(obj: Any) -> str:
artifact, raw = stored(obj)
files[artifact] = raw
return artifact
def load(artifact: str) -> Any:
raw = files.get(artifact)
return json.loads(raw) if raw is not None else None
queries = [(put(snap), snap) for snap in (AN_TOTALS, AN_REGIONS)]
# 照分析工具交给算子的写法:查询快照原样加上出处;目录事实(列的叫法、度量类型)按虚构的数据目录给,结论里记下
snapshots = {f"Q{i}": input_snapshot(snap, ref=f"Q{i}", artifact=sha, node_id="bot")
for i, (sha, snap) in enumerate(queries, 1)}
context = {"today": date(2026, 10, 19), "catalogs": {"shop": _shop_notes()}}
ledger: list[dict[str, Any]] = [
{"kind": "query", "node_id": "bot", "exec": 1, "artifact": sha, "call_id": f"call_{i}", "tool": "db_query__shop",
"source": "shop", "columns": snap["columns"], "rows": len(snap["rows"]), "truncated": False}
for i, (sha, snap) in enumerate(queries)]
results = []
for n, (tool, request) in enumerate(AN_CALLS, len(queries)):
result = dict(asyncio.run(analysis.run(request, snapshots.get, settings=effective_settings(None), context=context)))
artifact = put(result)
results.append((tool, f"call_{n}", artifact, result))
ledger.append(ledger_entry(result, node_id="bot", exec_no=1, artifact=artifact, call_id=f"call_{n}"))
catalog = build_catalog(nodes={}, ledger=ledger, loader=load)
doc = compose_doc(AN_REPORT, catalog, node_id="write", run_id=run["id"], loader=load)
doc["analysis_scope"] = analysis_scope.build(catalog, sources={"shop": {"tables": _shop_notes(), "masked": []}},
load=load)
doc_id = put(doc)
events: list[tuple[int, str, str | None, Any]] = []
def add(etype: str, node_id: str | None, data: dict[str, Any]) -> None:
events.append((len(events) + 1, etype, node_id, data))
add("run.started", None, {"nodes": len(graph["nodes"]), "resumed": False, "memory_scope": "default",
"collection": "default", "approval_default": None, "tool_trust": "formal",
"agent_limits": {}, "data_versions": None, "replay_protocol": 2})
add("node.started", "bot", {"node_type": "agent", "label": "问数据"})
for i, (sha, snap) in enumerate(queries):
add("tool.end", "bot", {"tool": "db_query__shop", "call_id": f"call_{i}", "duration_ms": 9, "query_artifact": sha,
"preview": json.dumps({"columns": snap["columns"], "rows": snap["rows"][:2]},
ensure_ascii=False)})
for k, (tool, call_id, artifact, result) in enumerate(results, 1):
add("tool.end", "bot", {"tool": tool, "call_id": call_id, "duration_ms": 6, "analysis_artifact": artifact,
"preview": json.dumps({"analysis_artifact": artifact}), "analysis": event_view(result, f"A{k}")})
add("node.finished", "bot", {"duration_ms": 240, "attempt": 1, "preview": {"text": "销售额下降,主要来自华东。"},
"evidence": ledger})
add("llm.end", "write", {"model": "mock", "output": "(写作草稿)"})
add("report.checked", "write", {"doc_artifact": doc_id, "ok": True})
add("node.finished", "write", {"duration_ms": 20, "attempt": 1, "preview": {"text": "(报告正文)", "doc_artifact": doc_id}})
add("issuance", "out", {"tier": "formal", "mode": "citations"})
add("node.finished", "out", {"duration_ms": 2, "attempt": 1, "preview": {"周报": "(报告正文)"}})
add("run.finished", None, {"output": {"周报": "(报告正文)"}})
return build_bundle(run, events, manifest_seq=len(events), manifest_hash=manifest_hash(events), read=files.get,
exported_at=EXPORTED_AT, graph=graph, source_masks={})
def bundles() -> dict[str, dict[str, Any]]: def bundles() -> dict[str, dict[str, Any]]:
"""{页面名: 页面数据}""" """{页面名: 页面数据}"""
out: dict[str, dict[str, Any]] = {} out: dict[str, dict[str, Any]] = {}
@@ -368,6 +491,7 @@ def bundles() -> dict[str, dict[str, Any]]:
out["formats"] = formats_bundle() out["formats"] = formats_bundle()
out["formats-unit"] = formats_bundle(card=FORMATS_UNIT_CARD) out["formats-unit"] = formats_bundle(card=FORMATS_UNIT_CARD)
out["prose"] = formats_bundle(text=PROSE) out["prose"] = formats_bundle(text=PROSE)
out["analysis"] = analysis_bundle()
return out return out
+13 -1
View File
@@ -470,7 +470,7 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
got = {name: json.loads(data_block(page)) for name, page in rendered.items()} got = {name: json.loads(data_block(page)) for name, page in rendered.items()}
assert set(got) == {"base", "two-reports", "metric-only", "legacy", "failed", "rejected", "continued", "degraded", assert set(got) == {"base", "two-reports", "metric-only", "legacy", "failed", "rejected", "continued", "degraded",
"missing-doc", "missing-card", "nan", "stray", "masked-card", "masked-old-doc", "masked-source", "missing-doc", "missing-card", "nan", "stray", "masked-card", "masked-old-doc", "masked-source",
"formats", "formats-unit", "prose"} "formats", "formats-unit", "prose", "analysis"}
for name, bundle in got.items(): for name, bundle in got.items():
assert browser_manifest(bundle["events"]) == bundle["meta"]["manifest_hash"], name assert browser_manifest(bundle["events"]) == bundle["meta"]["manifest_hash"], name
@@ -557,3 +557,15 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
# 口径卡单位带尾随空白:服务端把「元 」原样接在数后面,正文里的销售额带着那个空白 # 口径卡单位带尾随空白:服务端把「元 」原样接在数后面,正文里的销售额带着那个空白
shown = {s.get("ref"): s["text"] for s in segments(got["formats-unit"])} shown = {s.get("ref"): s["text"] for s in segments(got["formats-unit"])}
assert shown["m:gmv"] == "74,613.65元 " and shown["m:gmv|万"] == "7.46万元 " assert shown["m:gmv"] == "74,613.65元 " and shown["m:gmv|万"] == "7.46万元 "
# 异动周报:三份分析结论(导出时都复算一致)和两份输入快照随文件附带;文档带着系统写的分析范围,正文没引用的 A2、A3
# 只在范围里点名;分析工具的 tool.end 结论随文件附带了,带规范字符串
analysed = got["analysis"]
kinds = sorted((a["kind"], a.get("node_id"), "b64" in a) for a in analysed["artifacts"])
assert kinds == [("analysis_result", "bot", True)] * 3 + [("query", "bot", True)] * 2 + [("report_doc", "write", True)]
assert {a["recheck"]["status"] for a in analysed["artifacts"] if a["kind"] == "analysis_result"} == {"same"}
doc = json.loads(base64.b64decode(next(a["b64"] for a in analysed["artifacts"] if a["kind"] == "report_doc")))
scope = doc["analysis_scope"]
assert [[i["alias"] for i in scope[k]] for k in ("phenomena", "decomposed", "refused")] == [["A3"], ["A1"], ["A2"]]
assert scope["not_decomposed"] == ["渠道"] and scope["decomposed"][0]["dims"] == ["地区"]
assert {s["ref"] for s in segments(analysed) if s["ref"].startswith("a:")} == {"a:A1.top_share|pct"}
assert all("canonical" in e for e in analysed["events"] if e["type"] == "tool.end")
+79 -3
View File
@@ -22,7 +22,9 @@
// 判定、服务端缺文件、裸 NaN、别的运行的文档、口径卡有输入取自遮罩列(卡的节点完成事件只给哈希)、同一张卡配上证据 // 判定、服务端缺文件、裸 NaN、别的运行的文档、口径卡有输入取自遮罩列(卡的节点完成事件只给哈希)、同一张卡配上证据
// 目录里写着号码的老文档(文档只给哈希、页面写明原因)、设了遮罩的数据源上按地区汇总的查询、每种显示 // 目录里写着号码的老文档(文档只给哈希、页面写明原因)、设了遮罩的数据源上按地区汇总的查询、每种显示
// 格式各引用一处的经营月报、单位带尾随空白的经营月报、引用旁边是写作者自己写的普通文字的经营月报(「统一」「——」 // 格式各引用一处的经营月报、单位带尾随空白的经营月报、引用旁边是写作者自己写的普通文字的经营月报(「统一」「——」
// 「SKU-」「共…家」,原样导出全绿)),逐份断言各项结果和原因文案;有一项无法核对的样本页总览是中性的,不是绿的; // 「SKU-」「共…家」,原样导出全绿)、带分析结论和系统写的分析范围的异动周报(判定语、具名的数、结果表的格,范围里的
// 编号点得开)),逐份断言各项结果和原因文案;有一项无法核对的样本页总览是中性的,不是绿的;异动周报上只改判定语、具名的数
// 的显示并重算全部哈希,各自变红并点名;
// 经营月报上只改文字取值、数字、换算成万的口径卡引用的显示文字并重算全部哈希,各自变红并点名;核对过的字不动、改它 // 经营月报上只改文字取值、数字、换算成万的口径卡引用的显示文字并重算全部哈希,各自变红并点名;核对过的字不动、改它
// 旁边的(链接地址藏起数字、整段改成分隔线、文字片段冒用数字的 id;数字前面补一位数、加负号(半角、数学减号、负号 // 旁边的(链接地址藏起数字、整段改成分隔线、文字片段冒用数字的 id;数字前面补一位数、加负号(半角、数学减号、负号
// 前面是汉字的)、后面接「0 万」,隔着显示时不占位置的控制字符补一位数,这几种变红并点名「正文里紧挨着这个数的字改变了 // 前面是汉字的)、后面接「0 万」,隔着显示时不占位置的控制字符补一位数,这几种变红并点名「正文里紧挨着这个数的字改变了
@@ -611,7 +613,7 @@ await section('样本页:各种形状', async () => {
check('生成样本页(tests/bundle_samples.py)', false, String(e?.message ?? e)) check('生成样本页(tests/bundle_samples.py)', false, String(e?.message ?? e))
return return
} }
check('生成样本页(tests/bundle_samples.py)', Object.keys(samples).length === 18, Object.keys(samples).join('、')) check('生成样本页(tests/bundle_samples.py)', Object.keys(samples).length === 19, Object.keys(samples).join('、'))
await sample('base', async (s, page) => { await sample('base', async (s, page) => {
check('base:有无法核对的项,总览是中性的「可核对的项全部通过,另有 N 项无法核对」,不是绿色的通过', neutral(s), check('base:有无法核对的项,总览是中性的「可核对的项全部通过,另有 N 项无法核对」,不是绿色的通过', neutral(s),
@@ -904,6 +906,57 @@ await section('样本页:各种形状', async () => {
check('prose:17 个数字和取值都显示成可点的数字、都通过', s.numberStates.pass === 17 && s.numbers === 17, check('prose:17 个数字和取值都显示成可点的数字、都通过', s.numberStates.pass === 17 && s.numbers === 17,
JSON.stringify(s.numberStates)) JSON.stringify(s.numberStates))
}) })
// 异动周报(分析能力一轮):Agent 的三份分析结论(按地区的变化拆解、算不了的波动基线、两期可比性检查)和两份输入快照
// 随文件附带、导出时服务端复算过;报告引用拆解的判定语、具名的数、结果表的格(连同一整张结果表),正文后面是系统写的
// 「分析范围」。页面只核哈希、核判定语和数是不是结论里写的那几个字,不在浏览器里重算统计
await sample('analysis', async (s, page) => {
check('analysis:总览「全部通过」,没有一项无法核对', s.overall === 'pass' && s.text === '全部通过'
&& !s.items.some((i) => i.result !== 'pass'), `${s.overall}:${s.text};${describe(s)}`)
const kinds = s.items.filter((i) => i.check === 'artifacts').map((i) => i.text)
check('analysis:③ 三份分析结论、两份输入快照、报告文档都随文件附带、通过',
kinds.filter((t) => t.includes('分析结论')).length === 3 && kinds.filter((t) => t.includes('查询快照')).length === 2
&& kinds.some((t) => t.includes('报告文档')), kinds.join(' | '))
check('analysis:④ 输入快照按「已核对的分析结论的输入」认', hasItem(s, 'chain', 'pass', '分析结论(A1)的输入')
&& hasItem(s, 'chain', 'pass', '分析结论(A2)的输入'), s.items.filter((i) => i.check === 'chain').map((i) => i.text).join(' | '))
check('analysis:⑤ 分开数数字、取值、判定语', s.sums.numbers.includes('判定语 1 个'), s.sums.numbers)
const phrase = page.locator('.doc button.num[data-kind="analysis"]')
check('analysis:判定语是一个可点的数,读屏读作「判定语 …,通过」', await phrase.count() === 1
&& (await phrase.getAttribute('aria-label') ?? '').startsWith('判定语 销售额下降 194(-8.7%),主要来自华东(-120,占 61.9%),通过'),
await phrase.getAttribute('aria-label'))
const detail = await detailText(page, phrase)
check('analysis:点开判定语:判定语、判定标准、方法与版本(服务端带的叫法)', detail.includes('判定语')
&& detail.includes('一段占总变化的一半以上算主要来源') && detail.includes('变化拆解(可加) · decompose.additive@1'),
detail.replace(/\s+/g, ' ').slice(0, 300))
check('analysis:……附带说明、Agent 写的假设', detail.includes('附带说明') && detail.includes('Agent 写的假设')
&& detail.includes('下降是不是集中在某个地区'), detail.replace(/\s+/g, ' ').slice(0, 400))
check('analysis:……输入(角色、编号、各自的核对状态)', ['基期总数 · Q1', '本期总数 · Q1', '数据 · Q2'].every((t) => detail.includes(t)),
detail.replace(/\s+/g, ' ').slice(0, 500))
check('analysis:……复算写导出时服务端复算的结论、本页不重算统计', detail.includes('复算一致:按封存的输入重新计算,结论逐项相同')
&& detail.includes('本页只核哈希,不在浏览器里重算统计'), detail.replace(/\s+/g, ' ').slice(-300))
const share = page.locator('.doc p button.num').filter({ hasText: /^61\.9%$/ })
const shareDetail = await share.count() ? await detailText(page, share.first()) : '找不到「61.9%」'
check('analysis:具名的数(占比):出处写分析结论 A1 的具名的数「top_share」,和结论里记的值一致', shareDetail.includes('分析结论 A1 的具名的数「top_share」')
&& shareDetail.includes('与按显示规则渲染的结果一致'), shareDetail.replace(/\s+/g, ' ').slice(0, 300))
const cell = page.locator('.doc p button.num').filter({ hasText: /^-120$/ })
const cellDetail = await cell.count() ? await detailText(page, cell.first()) : '找不到「-120」'
check('analysis:结果表的格:出处写分析结论 A1 的结果表 · 第 1 行 · delta', cellDetail.includes('分析结论 A1 的结果表 · 第 1 行 · delta'),
cellDetail.replace(/\s+/g, ' ').slice(0, 300))
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('、'))
const scope = page.locator('[data-scope]')
const st = (await scope.innerText().catch(() => '')).replace(/\s+/g, ' ')
check('analysis:正文后面是系统写的「分析范围」:现象 A3、拆过 A1(按地区)、算不了 A2、没拆的渠道', st.includes('分析范围')
&& st.includes('写作者改不了') && /现象 A3/.test(st) && /拆过 A1(按地区)/.test(st) && /算不了 A2/.test(st) && /没拆 渠道/.test(st), st)
const a2 = scope.locator('button.num').filter({ hasText: /^A2$/ })
const a2Detail = await a2.count() ? await detailText(page, a2.first()) : '找不到 A2'
check('analysis:点范围里的 A2(正文没引用):随文件附带,看得到算不了的判定语和复算', a2Detail.includes('波动基线没有做成:只有 0 期历史,至少要 8 期')
&& a2Detail.includes('复算一致'), a2Detail.replace(/\s+/g, ' ').slice(0, 300))
check('analysis:……范围里的编号不核「正文显示」', !a2Detail.includes('正文显示'), a2Detail.replace(/\s+/g, ' ').slice(0, 300))
})
await sample('analysis', async (s) => {
check('analysis:390 宽不横向溢出', s.overflow <= 0, `${s.overflow}px`)
}, { width: 390 })
// 不执行脚本(附件预览、快速查看):封存哈希、运行 ID、工作流名照样看得到,状态写「尚未核对」,不是一直「正在核对」 // 不执行脚本(附件预览、快速查看):封存哈希、运行 ID、工作流名照样看得到,状态写「尚未核对」,不是一直「正在核对」
{ {
@@ -987,7 +1040,7 @@ await section('样本页:篡改', async () => {
data.artifacts.push({ id: card.id.replace(/^./, (c) => (c === '0' ? '1' : '0')), kind: 'retrieval', masked: true }) data.artifacts.push({ id: card.id.replace(/^./, (c) => (c === '0' ? '1' : '0')), kind: 'retrieval', masked: true })
return '多一份标着遮罩的检索快照' return '多一份标着遮罩的检索快照'
}, (s) => [...turnsRed('artifacts')(s), }, (s) => [...turnsRed('artifacts')(s),
['原因写明', hasItem(s, 'artifacts', 'fail', '只有查询快照、口径卡和报告文档会因遮罩不随文件附带')]]) ['原因写明', hasItem(s, 'artifacts', 'fail', '只有查询快照、口径卡、分析结论和报告文档会因遮罩不随文件附带')]])
// 遮罩卡在服务端被改过时,服务端照样只给 id、另标 mismatch(tests/test_bundle_masked_cards.py 守着服务端这一边): // 遮罩卡在服务端被改过时,服务端照样只给 id、另标 mismatch(tests/test_bundle_masked_cards.py 守着服务端这一边):
// 不能因为不嵌内容就和正常的遮罩卡一样「可核对的项全部通过」 // 不能因为不嵌内容就和正常的遮罩卡一样「可核对的项全部通过」
await tamper(samples['masked-card'], '遮罩卡标成服务端的文件与 id 不一致', (data) => { await tamper(samples['masked-card'], '遮罩卡标成服务端的文件与 id 不一致', (data) => {
@@ -1083,6 +1136,29 @@ await section('样本页:篡改', async () => {
...turnsRed('numbers', ['events', 'manifest', 'artifacts', 'chain', 'meta'])(s), ...turnsRed('numbers', ['events', 'manifest', 'artifacts', 'chain', 'meta'])(s),
['报告徽标不通过', s.reports[0]?.result === 'fail' && s.reports[0].parts.includes('不通过 1'), JSON.stringify(s.reports[0])], ['报告徽标不通过', s.reports[0]?.result === 'fail' && s.reports[0].parts.includes('不通过 1'), JSON.stringify(s.reports[0])],
], { seal: true }) ], { seal: true })
// 分析结论:只改正文显示的判定语、具名的数(引用不动、重算全部哈希),⑤ 里只有那一项不通过,原因点名正文显示的字
for (const [label, ref, change] of [['判定语', 'a:A1', (t) => t.replace('华东', '华南')], ['具名的数', 'a:A1.top_share|pct', () => '71.9%']]) {
await tamper(samples.analysis, `分析结论的${label}的显示并重算全部哈希`, (data) => rewriteDoc(data, (doc) => {
const seg = doc.blocks.flatMap((b) => b.units).flatMap((u) => u.segments).find((x) => x.ref === ref)
if (!seg) return null
const text = change(seg.text)
doc.markdown = doc.markdown.replace(seg.text, text)
seg.text = text
seg.cite.rendered = text
return { what: `片段 ${seg.id}(${ref})`, seg }
}), (s, forged) => {
const failed = s.items.filter((i) => i.check === 'numbers' && i.result === 'fail' && !i.folded)
return [
...turnsRed('numbers', ['events', 'manifest', 'artifacts', 'chain'])(s),
['⑤ 里只有这一项不通过', failed.length === 1, `${failed.length} 项`],
['原因点名正文显示的字', failed[0]?.text.includes(`正文显示「${forged.seg.text}」`), failed[0]?.text ?? '没有不通过的项'],
...(ref === 'a:A1' ? [['判定语另写结论里的判定语,不写「引用的值是空值」', failed[0]?.text.includes('分析结论里的判定语是「销售额下降 194(-8.7%),主要来自华东')
&& !failed[0]?.text.includes('空值'), failed[0]?.text ?? '']] : []),
['正文里那一处标成不通过', s.failedButtons.length === 1 && s.failedButtons[0].includes(`${forged.seg.text},不通过`),
s.failedButtons.join(' | ')],
]
}, { seal: true })
}
// 只改显示文字、引用值不动、重算全部哈希(验收里的 T4、T5):经营月报上改文字取值、千分位加货币单位的数字、换算成万的 // 只改显示文字、引用值不动、重算全部哈希(验收里的 T4、T5):经营月报上改文字取值、千分位加货币单位的数字、换算成万的
// 口径卡引用,各自只有那一项不通过。原因点名正文显示的字和引用的值,数字另写按显示规则应显示的字 // 口径卡引用,各自只有那一项不通过。原因点名正文显示的字和引用的值,数字另写按显示规则应显示的字
const shownForgeries = [ const shownForgeries = [