mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:01:58 +00:00
合入第 2 波复核修复 S3、S7、S8、S10、P1:分析卡上写在验证什么的那句照实说是模板作者写的、分析步骤的编号写明是哪一层、证据面板的格写列名与总数按服务端渲染、注释归位、窄屏收起工具栏的改名图标
This commit is contained in:
@@ -1015,8 +1015,12 @@ async def _chain(seg: dict[str, Any], doc: dict[str, Any], sealed: _Sealed,
|
||||
# 分析结论的出处链
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
#: Agent 写的假设(hypothesis)旁边那句:它不是结论,只在证据面板显示(细案 01 §7.3)
|
||||
HYPOTHESIS_NOTE = "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
#: 「在验证什么」(hypothesis)旁边那句:它不是结论,只在证据面板显示(细案 01 §7.3)。谁写的照实说:Agent 现场做的
|
||||
#: (台账条目带 call_id)写「Agent 写的假设」,分析卡做的(带 item)是模板作者写的,写「分析卡上写的『在验证什么』」
|
||||
HYPOTHESIS_LABEL = "Agent 写的假设"
|
||||
HYPOTHESIS_NOTE = f"{HYPOTHESIS_LABEL}:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
CARD_HYPOTHESIS_LABEL = "分析卡上写的『在验证什么』"
|
||||
CARD_HYPOTHESIS_NOTE = f"{CARD_HYPOTHESIS_LABEL}:模板作者写的那一句,不是分析结论,也不进报告正文"
|
||||
UNSEALED_ANALYSIS = "这份分析结论不属于封存范围内的任何事件(封存前的台账里没有它),不能作为证据展示"
|
||||
TAMPERED_ANALYSIS = "分析结论与哈希不一致,疑似被修改,不展示其中的判定和数"
|
||||
MISSING_ANALYSIS = "无法读取分析结论"
|
||||
@@ -1063,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]:
|
||||
"""这一处引用的是结论的哪一项:判定语(phrase)、具名标量(value:键、原值、换算)、结果表的一格(cell:行、列、原值)。"""
|
||||
"""这一处引用的是结论的哪一项:判定语(phrase)、具名标量(value:键、原值、换算)、结果表的一格(cell:行、列、原值)。
|
||||
|
||||
结果表的列写给人看:column 是稳定的列 id(share、delta……,引用里就是这么写的),column_label 是结果表记的列名
|
||||
(「占变化的比重」);页面照原样显示 column_label,不把列 id 当叫法。"""
|
||||
locator = cite.get("locator") or {}
|
||||
if cite.get("kind") == "cell":
|
||||
row, column = locator.get("row"), locator.get("column")
|
||||
@@ -1072,7 +1079,9 @@ def _analysis_cited(cite: dict[str, Any], result: dict[str, Any] | None) -> dict
|
||||
if table is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
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"):
|
||||
values = (result or {}).get("values") if isinstance((result or {}).get("values"), dict) else {}
|
||||
return {"what": "value", "key": locator["key"], "raw": values.get(locator["key"]),
|
||||
@@ -1104,11 +1113,13 @@ async def _analysis_chain(seg: dict[str, Any], cite: dict[str, Any], entry: dict
|
||||
"""分析结论的出处链(00-总方案 §3.3):判定 → 方法 → 计算 → 输入 → 复算 → 封存。
|
||||
|
||||
- 判定(analysis):判定语、判定标准、判定码和点名的段、附带说明、算不了的原因,这一处引用的是哪一项(判定语、具名标量、
|
||||
结果表的一格);Agent 写的假设另列,标成「Agent 写的假设」(HYPOTHESIS_NOTE)
|
||||
结果表的一格,列另写结果表记的列名 column_label);「在验证什么」另列,谁写的照实标:Agent 现场做的写「Agent 写的假设」
|
||||
(HYPOTHESIS_NOTE),分析卡做的写「分析卡上写的『在验证什么』」(CARD_HYPOTHESIS_NOTE,模板作者写的)
|
||||
- 方法(analysis_method):方法与版本、规范化后的参数、实际用的阈值(只列这个算子用到的)、现查的表范围(probes)
|
||||
- 计算(analysis_calc):结果表(前 table_top_k 段,加上被引用的那一行附近)、具名的数、关键数的代入式
|
||||
(evidence.analysis_formulas)
|
||||
- 输入(analysis_input,每份一步):落到报告目录的哪个 Q、用了哪些列和行、headline 那一格和结论里总数的闭合核对、
|
||||
- 输入(analysis_input,每份一步):落到报告目录的哪个 Q、用了哪些列和行、headline 那一格(值另由服务端渲染成 value_rendered,
|
||||
和闭合核对同一个写法)和结论里总数的闭合核对、
|
||||
下钻的上一层是哪个 A;后面接上每份输入快照的查询步骤(只认封存范围内交回过的,遮罩照查询步骤的办法)
|
||||
- 复算(analysis_recompute):按 sha256 取回封存的输入、用同一个函数复算,和封存的逐字段比(evidence.analysis_recheck);
|
||||
方法升过版、复算不了都照实写
|
||||
@@ -1154,7 +1165,11 @@ async def _analysis_chain(seg: dict[str, Any], cite: dict[str, Any], entry: dict
|
||||
phrase=result.get("phrase"), criterion=result.get("criterion"), verdict=verdict,
|
||||
caveats=[{k: c.get(k) for k in ("code", "text")} for c in result.get("caveats") or [] if isinstance(c, dict)])
|
||||
if isinstance(result.get("hypothesis"), str) and result["hypothesis"].strip():
|
||||
judged.update(hypothesis=result["hypothesis"], hypothesis_note=HYPOTHESIS_NOTE)
|
||||
# 谁写的照实说:分析卡(台账条目带 item)是模板作者写的,Agent 工具(带 call_id)是现场写的
|
||||
card = judged["origin"] == "card"
|
||||
judged.update(hypothesis=result["hypothesis"],
|
||||
hypothesis_label=CARD_HYPOTHESIS_LABEL if card else HYPOTHESIS_LABEL,
|
||||
hypothesis_note=CARD_HYPOTHESIS_NOTE if card else HYPOTHESIS_NOTE)
|
||||
settings = result.get("settings") if isinstance(result.get("settings"), dict) else {}
|
||||
method = {
|
||||
"step": "analysis_method", "alias": alias, "method": result.get("method"),
|
||||
@@ -1235,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,
|
||||
**{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":
|
||||
step["alias"] = _catalog_alias_of(catalog, artifact)
|
||||
parent, ok = _analysis_of(artifact)
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
dCell: '快照里的值', dShown: '正文显示', dShownSame: '与按显示规则渲染的结果一致', dExpected: '按显示规则应为',
|
||||
dArtifact: '所在工件', dReport: '所在报告',
|
||||
dPhrase: '判定语', dCriterion: '判定标准', dMethod: '方法与版本', dAnalysis: '分析结论里的值', dRecheck: '复算',
|
||||
dAnalysisInputs: '输入', dHypothesis: 'Agent 写的假设', dCaveats: '附带说明',
|
||||
dAnalysisInputs: '输入', dHypothesis: 'Agent 写的假设', dCardHypothesis: '分析卡上写的『在验证什么』', dCaveats: '附带说明',
|
||||
recheckBy: '导出时服务端按封存的输入复算的结论;本页只核哈希,不在浏览器里重算统计',
|
||||
recheckNone: '导出时没有复算(结论未随文件附带)',
|
||||
analysisInput: (role, alias) => `${role}${alias ? ` · ${alias}` : ''}`,
|
||||
@@ -1212,7 +1212,7 @@
|
||||
async function verify(data, digest, shownSeal) {
|
||||
const out = {
|
||||
events: [], manifest: [], artifacts: [], chain: [], numbers: [], docs: [], meta: [], checked: new Map(), order: [],
|
||||
arts: new Map(), queryAliases: new Map(),
|
||||
arts: new Map(), queryAliases: new Map(), analysisEntries: new Map(),
|
||||
}
|
||||
const events = Array.isArray(data.events) ? data.events.filter(isObj) : []
|
||||
const meta = data.meta
|
||||
@@ -1373,6 +1373,15 @@
|
||||
for (const [id, refs] of byDoc) {
|
||||
if (refs.length && !out.queryAliases.has(id)) out.queryAliases.set(id, refs[0].alias)
|
||||
}
|
||||
// 工件 → 报告目录里这条分析结论的条目(item、call_id):谁做的那一行、「在验证什么」的说法照它认
|
||||
out.analysisEntries = new Map()
|
||||
for (const [id, refs] of byDoc) {
|
||||
for (const ref of refs) {
|
||||
const doc = trusted.get(ref.report)
|
||||
const entry = own(isObj(doc) ? doc.catalog : null, ref.alias)
|
||||
if (isObj(entry) && entry.kind === 'analysis' && !out.analysisEntries.has(id)) out.analysisEntries.set(id, entry)
|
||||
}
|
||||
}
|
||||
out.arts = arts
|
||||
out.chained = chained
|
||||
// 已核对的口径卡记着的输入 → 输入指向的查询快照。卡本身核过哈希(③ 通过),它记的引用和文档的证据目录
|
||||
@@ -1452,6 +1461,7 @@
|
||||
const loc = isObj(cite.locator) ? cite.locator : {}
|
||||
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)) {
|
||||
// 结果表的格写列名:核对到结论之后换成结论记的列名(checkAnalysis 的 info.source),这里先照引用里的列 id 说
|
||||
return T.srcAnalysisCell(cite.alias, loc.row, String(loc.column ?? ''))
|
||||
}
|
||||
if (cite.kind === 'cell') {
|
||||
@@ -1564,6 +1574,9 @@
|
||||
const [raw, column, key] = locateCell(table, loc.row, loc.column)
|
||||
info.row = loc.row
|
||||
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.truthToken = tokenAt(tokensOf(art), ['table', 'rows', loc.row, key])
|
||||
if (typeof raw === 'string' && typeof info.truth === 'number') info.truthText = pyStrip(raw)
|
||||
@@ -1591,6 +1604,14 @@
|
||||
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 ""),不是文字的本页不认 */
|
||||
function unitOf(unit) {
|
||||
if (!unit) return ''
|
||||
@@ -2032,12 +2053,17 @@
|
||||
return parts
|
||||
}
|
||||
|
||||
/** 数字详情里分析结论一节:判定语、判定标准、方法与版本、附带说明、Agent 写的假设、输入(各自的核对状态)、导出时服务端
|
||||
* 的复算结论。结论没随文件附带(遮罩、文件不在)时只写复算一句 */
|
||||
/** 数字详情里分析结论一节:判定语、判定标准、方法与版本、附带说明、「在验证什么」(谁写的照实标:Agent 写的假设 /
|
||||
* 分析卡上写的『在验证什么』,照报告目录里这条结论的条目认)、输入(各自的核对状态)、导出时服务端的复算结论。
|
||||
* 结论没随文件附带(遮罩、文件不在)时只写复算一句 */
|
||||
function analysisBlock(number) {
|
||||
const result = isObj(number.analysis) ? number.analysis : null
|
||||
const art = view.out.artifacts.find((a) => a.a.id === number.artifact)
|
||||
const recheck = art && isObj(art.a.recheck) ? art.a.recheck : null
|
||||
// 谁做的:报告目录里这条结论的条目(带 item 的是分析卡做的,带 call_id 的是 Agent 现场做的)。「在验证什么」的说法照它认,
|
||||
// 不把模板作者写的那一句标成 Agent 写的假设
|
||||
const entry = view.out.analysisEntries.get(number.artifact)
|
||||
const card = isObj(entry) ? !!entry.item && !entry.call_id : false
|
||||
const parts = []
|
||||
if (result) {
|
||||
parts.push(kv([
|
||||
@@ -2048,7 +2074,7 @@
|
||||
const caveats = (Array.isArray(result.caveats) ? result.caveats : []).filter(isObj).map((c) => String(c.text || ''))
|
||||
if (caveats.length) parts.push(h('p', { class: 'sub' }, T.dCaveats), h('ul', { class: 'items' }, caveats.map((c) => h('li', {}, c))))
|
||||
if (typeof result.hypothesis === 'string' && result.hypothesis.trim()) {
|
||||
parts.push(h('p', { class: 'sub' }, T.dHypothesis), h('p', {}, result.hypothesis))
|
||||
parts.push(h('p', { class: 'sub' }, card ? T.dCardHypothesis : T.dHypothesis), h('p', {}, result.hypothesis))
|
||||
}
|
||||
const inputs = (Array.isArray(result.inputs) ? result.inputs : []).filter((i) => isObj(i) && typeof i.artifact === 'string')
|
||||
if (inputs.length) {
|
||||
|
||||
@@ -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]:
|
||||
"""台账里的一条分析结论 → 写作目录的 A 条目:照抄台账的键,带上从工件取回、复验过哈希的整份结论(result,
|
||||
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"), {}),
|
||||
"locator": {}, **{k: entry[k] for k in _ANALYSIS_KEYS if k in entry}}
|
||||
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:
|
||||
out["result"] = result
|
||||
table = result.get("table")
|
||||
if isinstance(table, dict) and isinstance(table.get("rows"), list):
|
||||
out["rows"] = len(table["rows"])
|
||||
if isinstance(table, dict):
|
||||
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 ""))
|
||||
name = method_label(out.get("op"), out.get("mode"))
|
||||
out["label"] = f"{name} · {phrase}" if phrase else name
|
||||
|
||||
@@ -462,9 +462,10 @@ async def run_supervisor(state: GraphState, ctx: NodeContext) -> dict[str, Any]:
|
||||
agent=name, call_id=cid, preview=content[:1500],
|
||||
duration_ms=int((time.perf_counter() - started) * 1000), **failure,
|
||||
**({"checks": checks} if checks else {}),
|
||||
# 判定的那一小段同 Agent 节点(event_view);编号是成员自己的分析容器里的,紧接着 _numbered 按它编
|
||||
# 判定的那一小段同 Agent 节点(event_view)。不带编号:成员自己的分析容器里的 A1 不是报告目录里的
|
||||
# A1(协作成员的结论还进不了台账),时间线上照它写「· A1」会和报告里点名的结论对不上
|
||||
**({"analysis_artifact": analysis["artifact"],
|
||||
"analysis": event_view(analysis["result"], scope.next_alias())} if analysis else {}))
|
||||
"analysis": event_view(analysis["result"])} if analysis else {}))
|
||||
if analysis is not None and analysis["result"].get("status") == "refused":
|
||||
ctx.emit(EventType.LOG, level="warn", code=ANALYSIS_REFUSED, agent=name, call_id=cid, tool=tname,
|
||||
reason_code=analysis["result"].get("reason_code"),
|
||||
|
||||
@@ -461,13 +461,15 @@
|
||||
}, v == null ? NONE : textOf(v))))))))
|
||||
}
|
||||
|
||||
/** 分析结论 · 判定:判定语、判定标准、判定码和点名的段、附带说明、算不了的原因、这一处引用的是哪一项、谁做的、Agent 写的假设 */
|
||||
/** 分析结论 · 判定:判定语、判定标准、判定码和点名的段、附带说明、算不了的原因、这一处引用的是哪一项、谁做的、
|
||||
* 「在验证什么」(谁写的照实标:Agent 写的假设 / 分析卡上写的『在验证什么』) */
|
||||
function analysisPart(step) {
|
||||
const body = []
|
||||
const verdict = isObj(step.verdict) ? step.verdict : null
|
||||
const cited = isObj(step.cited) ? step.cited : {}
|
||||
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([
|
||||
[T.dPhrase, String(step.phrase || NONE)],
|
||||
step.criterion ? [T.dCriterion, String(step.criterion)] : null,
|
||||
@@ -484,7 +486,9 @@
|
||||
h('ul', { style: 'margin:4px 0 0;padding-left:1.2em;font-size:13px' }, step.caveats.map((c) => h('li', {}, String(c.text || '')))))
|
||||
}
|
||||
if (step.hypothesis) {
|
||||
body.push(h('p', { class: 'sub' }, T.dHypothesis), h('p', { style: 'margin:4px 0 0;font-size:13px' }, String(step.hypothesis)))
|
||||
// 「在验证什么」:谁写的服务端照实给(Agent 写的假设 / 分析卡上写的『在验证什么』),这里照原样显示
|
||||
body.push(h('p', { class: 'sub' }, String(step.hypothesis_label || T.dHypothesis)),
|
||||
h('p', { style: 'margin:4px 0 0;font-size:13px' }, String(step.hypothesis)))
|
||||
if (step.hypothesis_note) body.push(muted(step.hypothesis_note))
|
||||
}
|
||||
if (step.note) body.push(waiting(step.note))
|
||||
@@ -558,7 +562,7 @@
|
||||
step.node_id ? [T.dNode, nodeLabel(step.node_id)] : null,
|
||||
Array.isArray(step.columns) && step.columns.length ? [T.dColumnsUsed, step.columns.join('、')] : 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,
|
||||
step.phrase ? [T.dPhrase, String(step.phrase)] : null,
|
||||
])]
|
||||
|
||||
@@ -100,6 +100,7 @@ def verdicts(bundle: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def test_the_analysis_and_its_input_travel_with_the_file_and_verify():
|
||||
"""Agent 现场做的结论(台账条目带 call_id):那一句「在验证什么」标成「Agent 写的假设」。"""
|
||||
bundle, files, a = bundle_of()
|
||||
analysis_item = item(bundle, a)
|
||||
assert analysis_item["kind"] == ARTIFACT_KIND and "b64" in analysis_item and analysis_item["node_id"] == "an"
|
||||
|
||||
@@ -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)
|
||||
stored = doc["catalog"]["A1"]
|
||||
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 verify_doc(doc, doc["catalog"], loader=store.load)["ok"]
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
真的 recompute:按 sha256 取回封存的输入快照、下钻的上一层结论,逐字段比。只有要它出错、升版的几处用 monkeypatch。
|
||||
|
||||
- 证据图:A1 条目带算子、判定;「派生自」的边 A1 → Q1
|
||||
- 片段接口:判定 → 方法 → 计算 → 输入(接查询步骤)→ 复算 → 封存;Agent 写的假设标出来;复算不了照实写,不让接口出错
|
||||
- 片段接口:判定 → 方法 → 计算 → 输入(接查询步骤)→ 复算 → 封存;「在验证什么」谁写的照实标(Agent 写的假设 / 分析卡上
|
||||
写的『在验证什么』);复算不了照实写,不让接口出错
|
||||
- 审计:分析判定、分析结论的叫法;分析说法核对的违规单列一行
|
||||
- 报告导出:数据块里有分析结论那几步;文档带「分析范围」的,正文后面渲染、编号点得开
|
||||
- 指标步骤带可比性:口径卡指标上的可比性结论(不进台账)照卡上记的 sha256 取回,点得开(判定 → … → 复算 → 封存)
|
||||
@@ -135,7 +136,9 @@ async def test_the_phrase_opens_judgment_method_calculation_inputs_recompute_and
|
||||
assert judged["verdict"] == {"code": "driver", "subjects": [["华东"]], "direction": "down"}
|
||||
assert judged["cited"] == {"what": "phrase"} and judged["render_ok"] is True and judged["eid_ok"] is True
|
||||
assert judged["origin"] == "card" and judged["item"] == "drop" and judged["label"] == "分析"
|
||||
assert judged["hypothesis"] == "华东拖累" and judged["hypothesis_note"].startswith("Agent 写的假设")
|
||||
# 分析卡做的:模板作者写的「在验证什么」,不标成 Agent 写的假设
|
||||
assert judged["hypothesis"] == "华东拖累" and judged["hypothesis_label"] == "分析卡上写的『在验证什么』"
|
||||
assert judged["hypothesis_note"].startswith("分析卡上写的『在验证什么』")
|
||||
assert {c["code"] for c in judged["caveats"]} == {"measure_unlabeled", "headline_basis", "comparability"}
|
||||
assert method["method"] == "decompose.additive@1" and method["method_label"] == "变化拆解(可加)"
|
||||
assert {s["key"]: s["label"] for s in method["settings"]}["main_share"] == "「主要来源」的占比"
|
||||
@@ -173,7 +176,8 @@ async def test_named_values_and_result_cells_point_at_what_they_cite(client):
|
||||
|
||||
cell = (await client.get(
|
||||
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["note"] == "该值由系统从分析结论的结果表中取出、按固定规则显示,未经模型转写"
|
||||
|
||||
@@ -216,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()
|
||||
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]}
|
||||
# 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)
|
||||
chain = data["chains"][data["refs"][segment["id"]]]
|
||||
@@ -307,9 +313,12 @@ def test_suspicious_name_hints_do_not_read_the_analysis_results(monkeypatch):
|
||||
|
||||
|
||||
async def test_agent_analyses_are_marked_as_such(client):
|
||||
"""Agent 现场做的:谁做的写 Agent,「在验证什么」照旧标「Agent 写的假设」(分析卡上那一句是模板作者写的,另说)"""
|
||||
made = await sealed_run(TEXT, item=None, call_id="call-9")
|
||||
body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'a:A1')}")).json()
|
||||
assert body["chain"][0]["origin"] == "agent" and body["chain"][0]["call_id"] == "call-9"
|
||||
judged = body["chain"][0]
|
||||
assert judged["origin"] == "agent" and judged["call_id"] == "call-9"
|
||||
assert judged["hypothesis_label"] == "Agent 写的假设" and judged["hypothesis_note"].startswith("Agent 写的假设")
|
||||
|
||||
|
||||
async def test_headline_inputs_show_the_closure(client):
|
||||
@@ -329,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",
|
||||
"headline": "2,026", "closed": True}
|
||||
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 next(s for s in body["chain"] if s["step"] == "analysis_recompute")["status"] == "same"
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import app.bundle
|
||||
import app.report_export
|
||||
from app.main import app as asgi
|
||||
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 (
|
||||
LAST_WEEK, SCOPE, TEXT, THIS_WEEK, comparability_card, ratio_run, sealed_run, seg_id,
|
||||
)
|
||||
@@ -147,13 +149,15 @@ async def test_the_export_page_opens_the_analysis_steps_and_the_scope_numbers():
|
||||
"analysis_recompute", "analysis_seal"]
|
||||
assert opened["title"].startswith("判定「销售额下降")
|
||||
body = opened["body"]
|
||||
for text in ("判定标准一段占总变化的一半以上算主要来源", "Agent 写的假设华东拖累", "实际用的阈值", "「主要来源」的占比",
|
||||
# 分析卡做的结论(台账条目带 item):那一句是模板作者写的,照实写「分析卡上写的『在验证什么』」,不标成 Agent 写的假设
|
||||
for text in ("判定标准一段占总变化的一半以上算主要来源", "分析卡上写的『在验证什么』华东拖累", "实际用的阈值", "「主要来源」的占比",
|
||||
"华东的占比 = 华东的变化 ÷ 总变化 = -120 ÷ (-194) = 61.9%", "结果表(列出 4 行,共 4 行)",
|
||||
"复算一致:按封存的输入重新计算,结论逐项相同", "封存范围内的台账记过这份分析结论",
|
||||
"按运行当天现算的界 date('now', '-7 days') = 2026-10-14(现查 · 2026-10-21T09:00:00)",
|
||||
"orders.refund_date:这一列一个值都没有(现查", "orders.order_date:2025-01-01 至 2026-10-20(剖析"):
|
||||
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"]
|
||||
|
||||
|
||||
@@ -168,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": [
|
||||
{"seg": phrase, "kind": "analysis", "state": "deterministic", "text": made["result"]["phrase"]}]})
|
||||
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 闭合"):
|
||||
assert text in body, text
|
||||
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():
|
||||
bundle, _, _ = bundle_of(doc_extra=SCOPE)
|
||||
out = run_node(RUN_VERIFIER, VERIFIER, {"data": json.dumps(bundle), "seal": bundle["meta"]["manifest_hash"]})
|
||||
@@ -186,6 +214,8 @@ async def test_the_bundle_page_shows_the_analysis_detail_and_the_scope():
|
||||
# 方法写服务端带来的叫法和 id(和导出页同一个写法),不只写 id
|
||||
assert "判定语销售额下降 194(-8.7%)" in phrase and "方法与版本变化拆解(可加) · decompose.additive@1" in phrase
|
||||
assert "输入数据 · Q1" in phrase # 输入的角色写叫法,不写 data
|
||||
# 这份结论是分析卡做的(台账条目带 item):那一句「在验证什么」写「分析卡上写的『在验证什么』」,不标成 Agent 写的假设
|
||||
assert "分析卡上写的『在验证什么』" in phrase and "Agent 写的假设" not in phrase
|
||||
assert "复算一致:按封存的输入重新计算,结论逐项相同(导出时服务端按封存的输入复算的结论;本页只核哈希" in phrase
|
||||
value = next(text for text in out["opened"].values() if "具名的数「top_share」" in text)
|
||||
assert "引用的值0.618556701031" in value and "正文显示61.9%(与按显示规则渲染的结果一致)" in value
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_tool_end_carries_the_phrase_and_the_same_number_the_model_got(mon
|
||||
assert error["call_error"] == "unknown_column" and "analysis" not in error
|
||||
|
||||
|
||||
async def test_a_team_member_numbers_its_own_analyses(monkeypatch, shop):
|
||||
async def test_a_team_member_analysis_carries_no_number(monkeypatch, shop):
|
||||
Script(monkeypatch, [(TOOL, {"sql": SQL}), (DECOMPOSE, DECOMPOSE_ARGS)],
|
||||
route={"assignments": [{"agent": "分析员", "instruction": "拆一下销售额的变化"}], "done": False})
|
||||
nodes = [node("start", "input"),
|
||||
@@ -82,7 +82,10 @@ async def test_a_team_member_numbers_its_own_analyses(monkeypatch, shop):
|
||||
input_payload={})
|
||||
row = await wait(run.id)
|
||||
assert row.status == "succeeded", row.error
|
||||
[end] = [e for e in await events(row.id, "tool.end", "team") if e.get("analysis_artifact")]
|
||||
assert end["agent"] == "分析员"
|
||||
assert (end["analysis"]["alias"], end["analysis"]["op"], end["analysis"]["verdict"]) == ("A1", "decompose", "driver")
|
||||
ends = [e for e in await events(row.id, "tool.end", "team") if e.get("analysis_artifact")]
|
||||
assert [e["agent"] for e in ends] == ["分析员"]
|
||||
end = ends[0]
|
||||
# 不带编号:成员自己的分析容器里的 A1 不是报告目录里的 A1(协作成员的结论还进不了台账),报告目录另按整次运行的台账编
|
||||
assert end["analysis"]["alias"] is None
|
||||
assert (end["analysis"]["op"], end["analysis"]["verdict"]) == ("decompose", "driver")
|
||||
assert json.dumps(end["analysis"], ensure_ascii=False).count("下降是不是集中在某个地区") == 0, "Agent 写的假设不带"
|
||||
|
||||
@@ -262,10 +262,13 @@ export function sourceOf(seg: EvidenceSegment, doc?: Pick<EvidenceDocData, 'cata
|
||||
if (!cite) return ''
|
||||
const entry = cite.alias ? doc?.catalog?.[cite.alias] : undefined
|
||||
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 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
|
||||
return [`${E.title} ${cite.alias ?? ''}`.trim(), what].join(' · ')
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* 的数、说法违规的原话都是服务端写好的,界面照原样显示,不按判定码、原因码自己拼。规矩同 terms.ts 开头的术语表。
|
||||
*
|
||||
* 术语:一次分析的结果叫「分析结论」,编号 A1、A2……;系统写的那一句叫「判定语」;判定的依据叫「判定标准」;做不成的叫「没有做成」
|
||||
* (服务端判定语的写法),原因叫「算不了的原因」;写错的调用叫「调用有误」,不记成结论;Agent 写的那句叫「Agent 写的假设」。
|
||||
* (服务端判定语的写法),原因叫「算不了的原因」;写错的调用叫「调用有误」,不记成结论;「这次在验证什么」那一句谁写的照实说:
|
||||
* Agent 现场做的那句叫「Agent 写的假设」,分析卡上那条是模板作者写的,叫「分析卡上写的『在验证什么』」。
|
||||
*/
|
||||
import { formatNumber } from './format'
|
||||
|
||||
@@ -221,8 +222,11 @@ export const ANALYSIS_TEXT = {
|
||||
step: {
|
||||
/** 运行中的状态字(store/chat) */
|
||||
running: (label: string) => `正在执行分析:${label}…`,
|
||||
/** 一步的标题:「变化拆解(可加)· A1」;分析卡的一项:「变化拆解(可加)· where」 */
|
||||
title: (method: string, alias?: string | null) => (alias ? `${method} · ${alias}` : method),
|
||||
/** 分析卡的一项:「变化拆解(可加)· where」。标识是这一项在分析卡里的 id,不是报告目录里的编号 */
|
||||
title: (method: string, item?: string | null) => (item ? `${method} · ${item}` : method),
|
||||
/** Agent 工具那一步:「变化拆解(可加)· 本节点 A1」。编号是这份 Agent 自己的分析容器里的(交回模型的「【分析 A1】」),
|
||||
* 报告目录里的 A1 按整次运行的台账编,两者不一定是同一个(图里还有分析卡时就不是) */
|
||||
nodeTitle: (method: string, alias?: string | null) => (alias ? `${method} · 本节点 ${alias}` : method),
|
||||
callError: '调用有误,没有记成结论',
|
||||
callErrorNext: '这次调用没有记成结论,Agent 会按这条说明改正参数后重新调用',
|
||||
refused: '没有做成',
|
||||
@@ -261,6 +265,7 @@ export const ANALYSIS_TEXT = {
|
||||
citedCell: (row: number, column: string) => `结果表第 ${formatNumber(row + 1)} 行「${column}」`,
|
||||
who: '谁做的',
|
||||
origin: { card: '分析卡(模板定好的分析)', agent: 'Agent 现场做的分析', metric: '口径卡执行时自动做的可比性检查' } as Record<string, string>,
|
||||
/** 「在验证什么」那一行:服务端按谁写的给说法(Agent 写的是假设,分析卡上那一条是模板作者写的),照原样显示 */
|
||||
hypothesis: 'Agent 写的假设',
|
||||
refused: '这次分析没有做成',
|
||||
renderOk: '与按结论渲染的结果一致',
|
||||
|
||||
@@ -527,9 +527,11 @@ export function StudioPage() {
|
||||
<span className="max-w-56 truncate font-medium @max-[1120px]:max-w-32">{workflow?.name ?? '选择工作流'}</span>
|
||||
<ChevronDown size={12} className="shrink-0 text-faint" />
|
||||
</button>
|
||||
{/* 改名只改名字,画布上没保存的改动不受影响;内置模板不给改名(见 renameWorkflow) */}
|
||||
{/* 改名只改名字,画布上没保存的改动不受影响;内置模板不给改名(见 renameWorkflow)。
|
||||
窄屏收起来:工具栏挤不下,改名在「切换工作流」的列表里照样有 */}
|
||||
{workflow && !workflow.is_template && (
|
||||
<IconButton label={`重命名「${workflow.name}」`} title="重命名" className="-ml-1 shrink-0 text-faint hover:text-dim"
|
||||
<IconButton label={`重命名「${workflow.name}」`} title="重命名"
|
||||
className="-ml-1 shrink-0 text-faint hover:text-dim @max-[880px]:hidden"
|
||||
onClick={() => void renameWorkflow(workflow, refresh)} icon={<Pencil size={12} />} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@ function JudgedPart({ step, recompute, seal, overall }: {
|
||||
const verdict = step.verdict && typeof step.verdict === 'object' ? step.verdict : null
|
||||
const cited = step.cited ?? {}
|
||||
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 trusted = !!overall?.sealed && overall.ok !== false
|
||||
const problems = [
|
||||
@@ -135,7 +136,7 @@ function JudgedPart({ step, recompute, seal, overall }: {
|
||||
{step.caveats.map((c, i) => <li key={`${c.code ?? ''}-${i}`}>{c.text}</li>)}
|
||||
</ul>
|
||||
)] : null,
|
||||
step.hypothesis ? [E.hypothesis, (
|
||||
step.hypothesis ? [step.hypothesis_label || E.hypothesis, (
|
||||
<span key="h" data-ev-analysis-hypothesis="">
|
||||
{step.hypothesis}
|
||||
{step.hypothesis_note && <span className="mt-0.5 block text-2xs text-faint">{step.hypothesis_note}</span>}
|
||||
@@ -267,7 +268,9 @@ function InputPart({ step, onQuery, trusted }: { step: EvidenceAnalysisStep; onQ
|
||||
rows ? [E.rowsUsed, rows] : null,
|
||||
'value' in step && !parent ? [ratio ? E.headlineRatio : E.headline, (
|
||||
<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>
|
||||
)] : null,
|
||||
closure && closed ? [E.closure, (
|
||||
|
||||
@@ -423,7 +423,16 @@
|
||||
"op": "decompose",
|
||||
"rows": 4,
|
||||
"status": "ok",
|
||||
"verdict": "driver"
|
||||
"verdict": "driver",
|
||||
"labels": {
|
||||
"base": "基期",
|
||||
"delta": "变化",
|
||||
"flag": "说明",
|
||||
"pct": "变化率",
|
||||
"region": "地区",
|
||||
"share": "占变化的比重",
|
||||
"target": "本期"
|
||||
}
|
||||
},
|
||||
"A2": {
|
||||
"alias": "A2",
|
||||
@@ -460,7 +469,14 @@
|
||||
"op": "comparable",
|
||||
"rows": 5,
|
||||
"status": "ok",
|
||||
"verdict": "comparable"
|
||||
"verdict": "comparable",
|
||||
"labels": {
|
||||
"base": "基期",
|
||||
"check": "检查项",
|
||||
"note": "说明",
|
||||
"status": "结果",
|
||||
"target": "本期"
|
||||
}
|
||||
},
|
||||
"Q1": {
|
||||
"alias": "Q1",
|
||||
@@ -673,7 +689,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "Agent 写的假设"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -897,7 +914,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -918,7 +936,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -1300,7 +1319,8 @@
|
||||
"what": "cell",
|
||||
"row": 0,
|
||||
"column": "delta",
|
||||
"raw": "-300"
|
||||
"raw": "-300",
|
||||
"column_label": "变化"
|
||||
},
|
||||
"rendered": "-300",
|
||||
"render_ok": true,
|
||||
@@ -1326,7 +1346,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "Agent 写的假设"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -1555,7 +1576,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -1576,7 +1598,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -1984,7 +2007,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "Agent 写的假设"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -2209,7 +2233,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -2230,7 +2255,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -3280,7 +3306,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "Agent 写的假设"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -3504,7 +3531,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -3525,7 +3553,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -3871,7 +3900,8 @@
|
||||
"verdict": null,
|
||||
"caveats": [],
|
||||
"hypothesis": "这周的下降算不算异常",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "Agent 写的假设"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -4075,7 +4105,8 @@
|
||||
},
|
||||
"caveats": [],
|
||||
"hypothesis": "两周能不能直接比",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "Agent 写的假设"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -5028,7 +5059,16 @@
|
||||
"op": "decompose",
|
||||
"rows": 4,
|
||||
"status": "ok",
|
||||
"verdict": "driver"
|
||||
"verdict": "driver",
|
||||
"labels": {
|
||||
"base": "基期",
|
||||
"delta": "变化",
|
||||
"flag": "说明",
|
||||
"pct": "变化率",
|
||||
"region": "地区",
|
||||
"share": "占变化的比重",
|
||||
"target": "本期"
|
||||
}
|
||||
},
|
||||
"Q1": {
|
||||
"alias": "Q1",
|
||||
@@ -5189,7 +5229,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "分析卡上写的『在验证什么』:模板作者写的那一句,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "分析卡上写的『在验证什么』"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -5413,7 +5454,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -5434,7 +5476,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -6056,7 +6099,8 @@
|
||||
"what": "cell",
|
||||
"row": 0,
|
||||
"column": "delta",
|
||||
"raw": "-300"
|
||||
"raw": "-300",
|
||||
"column_label": "变化"
|
||||
},
|
||||
"rendered": "-300",
|
||||
"render_ok": true,
|
||||
@@ -6082,7 +6126,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "分析卡上写的『在验证什么』:模板作者写的那一句,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "分析卡上写的『在验证什么』"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -6311,7 +6356,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -6332,7 +6378,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -6714,7 +6761,8 @@
|
||||
"what": "cell",
|
||||
"row": 0,
|
||||
"column": "delta",
|
||||
"raw": "-300"
|
||||
"raw": "-300",
|
||||
"column_label": "变化"
|
||||
},
|
||||
"rendered": "-300",
|
||||
"render_ok": true,
|
||||
@@ -6740,7 +6788,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "分析卡上写的『在验证什么』:模板作者写的那一句,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "分析卡上写的『在验证什么』"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -6969,7 +7018,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -6990,7 +7040,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -7924,7 +7975,8 @@
|
||||
}
|
||||
],
|
||||
"hypothesis": "下降是不是集中在某个地区",
|
||||
"hypothesis_note": "Agent 写的假设:说明这次在验证什么,不是分析结论,也不进报告正文"
|
||||
"hypothesis_note": "分析卡上写的『在验证什么』:模板作者写的那一句,不是分析结论,也不进报告正文",
|
||||
"hypothesis_label": "分析卡上写的『在验证什么』"
|
||||
},
|
||||
{
|
||||
"step": "analysis_method",
|
||||
@@ -8148,7 +8200,8 @@
|
||||
"total": "2,700",
|
||||
"headline": "2,700",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,700"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -8169,7 +8222,8 @@
|
||||
"total": "2,360",
|
||||
"headline": "2,360",
|
||||
"closed": true
|
||||
}
|
||||
},
|
||||
"value_rendered": "2,360"
|
||||
},
|
||||
{
|
||||
"step": "analysis_input",
|
||||
@@ -9453,4 +9507,4 @@
|
||||
"declared_at": "2026-10-21T10:36:37.326911+00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,13 +739,13 @@ export function analysisTeamRun(): RunEvent[] {
|
||||
ev('tool.end', 'team', { tool: 'analysis_decompose', agent: '分析员', call_id: 'm2', duration_ms: 8,
|
||||
preview: '{\n "analysis_artifact": "a1f0", \n "result": {\n "op": "decompose",\n "mode": "additive",\n "method": "decompose.additive@1",',
|
||||
analysis_artifact: 'a1f0c3d2b4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9',
|
||||
analysis: { alias: 'A1', op: 'decompose', mode: 'additive', status: 'ok', verdict: 'driver', direction: 'down', phrase: TEAM_PHRASE,
|
||||
analysis: { alias: null, op: 'decompose', mode: 'additive', status: 'ok', verdict: 'driver', direction: 'down', phrase: TEAM_PHRASE,
|
||||
reason_code: null } }, 0.01)
|
||||
ev('tool.start', 'team', { tool: 'analysis_baseline', agent: '分析员', call_id: 'm3', args: { query: 'Q1', period: 'week', measure: 'gmv' } }, 0.1)
|
||||
ev('tool.end', 'team', { tool: 'analysis_baseline', agent: '分析员', call_id: 'm3', duration_ms: 4,
|
||||
preview: '{\n "analysis_artifact": "b2e1",',
|
||||
analysis_artifact: 'b2e1d4c3a5f6e70819203b4c5d6e7f8091a2b3c4d5e6f70819203b4c5d6e7f80',
|
||||
analysis: { alias: 'A2', op: 'baseline', mode: null, status: 'refused', verdict: null, direction: null,
|
||||
analysis: { alias: null, op: 'baseline', mode: null, status: 'refused', verdict: null, direction: null,
|
||||
phrase: '波动基线没有做成:只有 0 期历史,至少要 8 期', reason_code: 'history_short' } }, 0.01)
|
||||
ev('log', 'team', { level: 'warn', code: 'analysis_refused', agent: '分析员', call_id: 'm3', tool: 'analysis_baseline',
|
||||
reason_code: 'history_short', reason: '只有 0 期历史,至少要 8 期', message: '分析员 的 analysis_baseline 没有做成:只有 0 期历史,至少要 8 期' })
|
||||
|
||||
@@ -1387,8 +1387,9 @@ function toolStep(seq: number, tool: string, args: Record<string, any>): Step {
|
||||
}
|
||||
|
||||
/**
|
||||
* tool.end 的 analysis(tools/analysis.event_view)折进分析那一行:标题接上编号,副标题是判定语(算不了的就是「…没有做成:原因」,
|
||||
* 服务端写好的);算不了的用提醒色。老服务端没有这一段:preview 只是结论的前一截,判定语多半被截掉,只照参数写叫法,不从里面拼
|
||||
* tool.end 的 analysis(tools/analysis.event_view)折进分析那一行:标题接上这份 Agent 自己的编号(「本节点 A1」),副标题是
|
||||
* 判定语(算不了的就是「…没有做成:原因」,服务端写好的);算不了的用提醒色。老服务端没有这一段:preview 只是结论的前一截,
|
||||
* 判定语多半被截掉,只照参数写叫法,不从里面拼
|
||||
*/
|
||||
function settleAnalysis(step: Step, d: Record<string, any>): void {
|
||||
const view = d.analysis && typeof d.analysis === 'object' ? d.analysis as Record<string, any> : null
|
||||
@@ -1403,7 +1404,9 @@ function settleAnalysis(step: Step, d: Record<string, any>): void {
|
||||
if (typeof view.reason_code === 'string') info.reasonCode = view.reason_code
|
||||
}
|
||||
step.analysis = info
|
||||
step.title = ANALYSIS_TEXT.step.title(analysisMethodLabel(info.op, info.mode), info.alias)
|
||||
// Agent 工具那一步的编号是这份 Agent 自己的分析容器里的(交回模型的「【分析 A1】」),报告目录里的 A1 按整次运行的台账编,
|
||||
// 两者不一定相同(图里还有分析卡时就不是):标题写「本节点 A1」,不冒充报告里的编号
|
||||
step.title = ANALYSIS_TEXT.step.nodeTitle(analysisMethodLabel(info.op, info.mode), info.alias)
|
||||
if (info.phrase) step.sub = step.sub ? `${info.phrase} · ${step.sub}` : info.phrase
|
||||
if (info.status === 'refused') {
|
||||
step.level = 'warn'
|
||||
|
||||
+15
-3
@@ -2435,8 +2435,16 @@ export interface EvidenceAnalysisStep {
|
||||
artifact?: string | null
|
||||
eid?: string
|
||||
eid_ok?: boolean
|
||||
/** 这一处引用的是结论的哪一项:判定语、具名的数(key)、结果表的一格(row、column) */
|
||||
cited?: { what?: 'phrase' | 'value' | 'cell' | string; key?: string; row?: number; column?: string; raw?: unknown; conv?: string }
|
||||
/** 这一处引用的是结论的哪一项:判定语、具名的数(key)、结果表的一格(row、column;column_label 是结果表的列名,给人看的) */
|
||||
cited?: {
|
||||
what?: 'phrase' | 'value' | 'cell' | string
|
||||
key?: string
|
||||
row?: number
|
||||
column?: string
|
||||
column_label?: string | null
|
||||
raw?: unknown
|
||||
conv?: string
|
||||
}
|
||||
rendered?: string
|
||||
render_ok?: boolean
|
||||
hash_ok?: boolean | null
|
||||
@@ -2448,6 +2456,8 @@ export interface EvidenceAnalysisStep {
|
||||
verdict?: AnalysisVerdict | null
|
||||
caveats?: AnalysisCaveat[]
|
||||
hypothesis?: string
|
||||
/** 「在验证什么」那一行的说法:谁写的照实说,服务端按台账条目给(Agent 写的假设 / 分析卡上写的『在验证什么』) */
|
||||
hypothesis_label?: string
|
||||
hypothesis_note?: string
|
||||
note?: string
|
||||
// ---- analysis_method:方法、参数、实际用的阈值、现查结果 ----
|
||||
@@ -2479,6 +2489,8 @@ export interface EvidenceAnalysisStep {
|
||||
row?: number
|
||||
column?: string
|
||||
value?: string | null
|
||||
/** 那一格的值按服务端的规则渲染出来的字(render_cell:千分位、NULL 写「—」),和闭合核对里的写法一致 */
|
||||
value_rendered?: string
|
||||
/** 输入落到报告目录的哪个 Q */
|
||||
query?: string | null
|
||||
sealed?: boolean
|
||||
@@ -4589,7 +4601,6 @@ export interface RunCompareQuery {
|
||||
problem?: string
|
||||
}
|
||||
|
||||
/** GET /runs/{id}/compare?base=。任一未封存或封存核对不一致 409;不是同一工作流、是同一次运行 422 */
|
||||
/** 运行对比里分析卡的一边(analysis.end 那一项):没有这一项为 null */
|
||||
export interface RunCompareAnalysisSide {
|
||||
status?: string | null
|
||||
@@ -4614,6 +4625,7 @@ export interface RunCompareAnalysis {
|
||||
change: 'same' | 'verdict' | 'phrase' | 'added' | 'removed' | string
|
||||
}
|
||||
|
||||
/** GET /runs/{id}/compare?base=。任一未封存或封存核对不一致 409;不是同一工作流、是同一次运行 422 */
|
||||
export interface RunComparison {
|
||||
target: RunCompareRef
|
||||
base: RunCompareRef
|
||||
|
||||
@@ -927,8 +927,9 @@ await section('样本页:各种形状', async () => {
|
||||
check('analysis:点开判定语:判定语、判定标准、方法与版本(服务端带的叫法)', detail.includes('判定语')
|
||||
&& detail.includes('一段占总变化的一半以上算主要来源') && detail.includes('变化拆解(可加) · decompose.additive@1'),
|
||||
detail.replace(/\s+/g, ' ').slice(0, 300))
|
||||
// 这三份分析都是 Agent 现场做的(台账条目带 call_id):那一句照旧标「Agent 写的假设」
|
||||
check('analysis:……附带说明、Agent 写的假设', detail.includes('附带说明') && detail.includes('Agent 写的假设')
|
||||
&& detail.includes('下降是不是集中在某个地区'), detail.replace(/\s+/g, ' ').slice(0, 400))
|
||||
&& !detail.includes('分析卡上写的『在验证什么』') && 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('复算一致:按封存的输入重新计算,结论逐项相同')
|
||||
@@ -939,7 +940,8 @@ await section('样本页:各种形状', async () => {
|
||||
&& 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'),
|
||||
// 列写结果表记的列名(「变化」),不把列 id(delta)当叫法
|
||||
check('analysis:结果表的格:出处写分析结论 A1 的结果表 · 第 1 行 · 变化', cellDetail.includes('分析结论 A1 的结果表 · 第 1 行 · 变化'),
|
||||
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('、'))
|
||||
|
||||
@@ -127,17 +127,17 @@ await section('分析:Agent 的分析工具、分析卡的每一项', async ()
|
||||
&& wrong.sub === '调用有误,没有记成结论' && !!wrong.next?.includes('Agent 会按这条说明改正参数'), `${wrong?.status} ${wrong?.sub}`)
|
||||
check('……服务端的原话留在展开区,调用错误的代码记在这一行', !!wrong?.result?.includes('Q2 里没有「sales」这一列')
|
||||
&& wrong.analysis?.callError === 'unknown_column', wrong?.result)
|
||||
check('做成的:标题接上本节点的编号(A1,和交回模型的「【分析 A1】」同一个)', decompose?.title === '变化拆解(可加) · A1', decompose?.title)
|
||||
check('做成的:标题接上本节点的编号(A1,和交回模型的「【分析 A1】」同一个)', decompose?.title === '变化拆解(可加) · 本节点 A1', decompose?.title)
|
||||
check('……副标题是服务端渲染的判定语', decompose?.sub === '销售额下降 340(-12.6%),主要来自华东(-300,占 88.2%)', decompose?.sub)
|
||||
check('……结论工件可下钻,Agent 写的假设另记、不进参数', !!decompose?.artifact && decompose.analysis?.hypothesis === '下降是不是集中在某个地区'
|
||||
&& !decompose.detail?.includes('hypothesis') && !decompose.detail?.includes('null'), decompose?.detail?.slice(0, 80))
|
||||
check('……preview(结论 JSON 的前一截)不摆出来', !decompose?.result, decompose?.result?.slice(0, 40))
|
||||
check('算不了的:一条结论(不是失败),提醒色,判定语就是「…没有做成:原因」', baseline?.status === 'done' && baseline.level === 'warn'
|
||||
&& baseline.title === '波动基线 · A2' && baseline.sub === '波动基线没有做成:只有 0 期历史,至少要 8 期', `${baseline?.title} ${baseline?.sub}`)
|
||||
&& baseline.title === '波动基线 · 本节点 A2' && baseline.sub === '波动基线没有做成:只有 0 期历史,至少要 8 期', `${baseline?.title} ${baseline?.sub}`)
|
||||
check('……下一步说能照实写原因、不能据此下判断', !!baseline?.next?.includes('算不了也是一条结论'))
|
||||
check('……紧跟着的「没有做成」提醒折进这一行,不另起一行', !all.some((s) => s.code === 'analysis_refused'),
|
||||
all.filter((s) => s.code === 'analysis_refused').map((s) => s.title).join(' | '))
|
||||
check('可比性检查也有编号和判定语', comparable?.title === '可比性检查 · A3' && !!comparable.sub?.startsWith('两期可比'), comparable?.title)
|
||||
check('可比性检查也有编号和判定语', comparable?.title === '可比性检查 · 本节点 A3' && !!comparable.sub?.startsWith('两期可比'), comparable?.title)
|
||||
check('分析那几行是正常的子步骤:不当成运行出错(运行照样完成)', all.some((s) => s.kind === 'lifecycle' && s.title === '完成'))
|
||||
const claims = all.find((s) => s.code === 'report_analysis_claims')
|
||||
check('报告的分析说法重写后仍有:说成人话(几处、已在报告中标注),副标题点名判断词', claims?.title === '报告有 3 处分析说法未通过核对,已在报告中标注'
|
||||
@@ -170,8 +170,8 @@ await section('分析:Agent 的分析工具、分析卡的每一项', async ()
|
||||
|
||||
// 协作成员的分析:编号是成员自己的,副标题接着写是谁调用的
|
||||
const team = flatten(mod.decodeRun(synthetic.analysisTeamRun(), { status: 'succeeded' })).filter((s) => s.kind === 'analysis')
|
||||
check('协作成员调分析工具:编号是成员自己的,副标题写判定语和谁调用的', team[0]?.title === '变化拆解(可加) · A1'
|
||||
&& team[0].sub === `${synthetic.TEAM_PHRASE} · 分析员 调用` && team[1]?.title === '波动基线 · A2', team.map((s) => `${s.title}:${s.sub}`).join(' | '))
|
||||
check('协作成员调分析工具:不带编号(成员容器里的 A1 不是报告目录里的),副标题写判定语和谁调用的', team[0]?.title === '变化拆解(可加)'
|
||||
&& team[0].sub === `${synthetic.TEAM_PHRASE} · 分析员 调用` && team[1]?.title === '波动基线', team.map((s) => `${s.title}:${s.sub}`).join(' | '))
|
||||
})
|
||||
|
||||
await section('合并查询', async () => {
|
||||
|
||||
@@ -619,6 +619,9 @@ await section('analysis', '分析结论:判定语、具名的数、结果表
|
||||
=== JSON.stringify(['headline_base', 'headline_target', 'data']))
|
||||
check('……总数那一格和各段之和的闭合核对(服务端算好的数)', (await inputs.nth(0).innerText()).includes('总数 2,700 与各段之和 2,700 闭合')
|
||||
&& 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('输入快照的查询步骤接在后面,写明是 A1 的输入', await p.locator('[data-ev-input-of="A1"]').count() === 2)
|
||||
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 box.locator('[data-seg="s7"]').click()
|
||||
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)
|
||||
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(','))
|
||||
|
||||
// 分析卡的结论:谁做的写分析卡(模板定好的分析);表格里引用结果表的格
|
||||
@@ -645,6 +649,10 @@ await section('analysis', '分析结论:判定语、具名的数、结果表
|
||||
await p.locator('[data-ev-analysis="A2"]').waitFor({ timeout: 8000 })
|
||||
check('分析卡的结论:谁做的写「分析卡(模板定好的分析)」', (await p.locator('[data-ev-analysis-origin]').getAttribute('data-ev-analysis-origin')) === 'card'
|
||||
&& (await p.locator('[data-ev-analysis="A2"]').innerText()).includes('分析卡(模板定好的分析)'))
|
||||
// 「在验证什么」那一句是模板作者写的:照实写「分析卡上写的『在验证什么』」,不标成 Agent 写的假设
|
||||
const cardBody = await p.locator('[data-ev-analysis="A2"]').innerText()
|
||||
check('……「在验证什么」写「分析卡上写的『在验证什么』」,不说 Agent 写的假设', cardBody.includes('分析卡上写的『在验证什么』')
|
||||
&& !cardBody.includes('Agent 写的假设'), cardBody.slice(-160))
|
||||
await card.locator('[data-seg="s6"]').click()
|
||||
await p.locator('[data-ev-analysis="A1"]').waitFor({ timeout: 8000 })
|
||||
check('算不了的结论:标「这次分析没有做成」、写算不了的原因', await p.locator('[data-ev-analysis-refused]').count() === 1
|
||||
|
||||
@@ -748,7 +748,7 @@ await section('分析:Agent 的分析工具、分析卡的每一项一行,
|
||||
&& await page.locator('[data-step-kind="analysis"]:not([data-step-status="failed"]) svg.lucide-chart-column-stacked').count() === 5,
|
||||
String(await rows.count()))
|
||||
const line = async (i) => (await rows.nth(i).innerText().catch(() => '')).replace(/\s+/g, ' ')
|
||||
check('做成的一行:标题写算子的叫法和编号,副标题是判定语', (await line(1)).includes('变化拆解(可加) · A1')
|
||||
check('做成的一行:标题写算子的叫法和本节点的编号(报告目录里的编号另算),副标题是判定语', (await line(1)).includes('变化拆解(可加) · 本节点 A1')
|
||||
&& (await line(1)).includes('主要来自华东(-300,占 88.2%)'), await line(1))
|
||||
check('……标题里不出现工具名 analysis_decompose', !(await page.locator('[data-turn]').first().innerText()).includes('analysis_decompose'))
|
||||
await rows.nth(1).locator('button').first().click()
|
||||
@@ -790,7 +790,8 @@ await section('分析:Agent 的分析工具、分析卡的每一项一行,
|
||||
|
||||
const team = await open('syn=analysis-team', { w: 1100, h: 900 })
|
||||
const tl = (await team.page.locator('[data-step-kind="analysis"]').first().innerText().catch(() => '')).replace(/\s+/g, ' ')
|
||||
check('协作成员的分析:编号是成员自己的,副标题接着写是谁调用的', tl.includes('变化拆解(可加) · A1') && tl.includes('分析员 调用'), tl)
|
||||
check('协作成员的分析:不带编号(进不了台账,报告目录里的 A1 另算),副标题接着写是谁调用的', tl.includes('变化拆解(可加)') && !tl.includes('本节点 A1')
|
||||
&& tl.includes('分析员 调用'), tl)
|
||||
await team.page.close()
|
||||
})
|
||||
|
||||
|
||||
@@ -846,6 +846,14 @@ await section('窄屏:次要按钮收进「更多」、助手栏默认收起
|
||||
check('768 宽:最右的控件也在视口里、工具栏不溢出', bar.right <= bar.vw && bar.sw <= bar.cw, JSON.stringify({ right: bar.right, vw: bar.vw, sw: bar.sw, cw: bar.cw }))
|
||||
check('768 宽:运行和收起助手栏都露在外面', bar.names.includes('运行') && bar.names.some((n) => n.includes('助手栏')), bar.names.join(' '))
|
||||
|
||||
// 改名那个小铅笔收起来了(768 宽挤不下):改名还在「切换工作流」的列表里
|
||||
check('768 宽:工具栏上看不到改名的图标(挤不下)', !bar.names.some((n) => n.includes('重命名')), bar.names.join(' '))
|
||||
await page.locator('button[title^="切换、新建"]').click()
|
||||
await page.waitForTimeout(250)
|
||||
check('……改名在「切换工作流」的列表里照样有', await count(page, '[role="listbox"][aria-label="工作流"] [aria-label^="重命名"]') > 0)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
const more = page.getByRole('button', { name: '更多操作' })
|
||||
check('窄屏把次要按钮收进「更多操作」', await more.count() === 1 && await more.isVisible())
|
||||
await more.click()
|
||||
|
||||
Reference in New Issue
Block a user