合入第 2 波复核修复 S3、S7、S8、S10、P1:分析卡上写在验证什么的那句照实说是模板作者写的、分析步骤的编号写明是哪一层、证据面板的格写列名与总数按服务端渲染、注释归位、窄屏收起工具栏的改名图标

This commit is contained in:
ThearchyHelios
2026-10-21 16:17:20 +00:00
23 changed files with 298 additions and 95 deletions
+5 -2
View File
@@ -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 -3
View File
@@ -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: '与按结论渲染的结果一致',
+4 -2
View File
@@ -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} />} />
)}
+6 -3
View File
@@ -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"
}
}
}
}
+2 -2
View File
@@ -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 期' })
+6 -3
View File
@@ -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
View File
@@ -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