mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:41:57 +00:00
证据面板与报告正文接上分析结论(分析能力一轮)。点判定语、具名的数、结果表的格:出处链照服务端的分析几步(判定 → 方法 → 计算 → 输入 → 复算 → 封存)——判定写编号和方法的叫法(method_label)、判定语照原样、判定标准、点名的段、方向、谁做的(Agent 现场 / 分析卡 / 口径卡执行时的可比性检查)、附带说明、Agent 写的假设(另说它不是结论);方法写版本、实际用的阈值(契约的叫法)、规范化后的参数;计算写代入式、具名的数、结果表(被引用的数描出来、那一格高亮);输入每份一节,总数和各段之和的闭合核对照服务端算好的数,输入快照的查询步骤接在后面、点 Q 跳过去;复算一致与否、封存范围内台账记没记过、算不了的写原因;分析结论的数不走口径账本。口径卡指标上多一行「两期可比性」(判定、判定语,展开看那份可比性结论的整条链,随卡封存)。说法违规(判断词未挂分析结论、判断与分析结论不符、因果说法没有依据,按 unit / segment / span)标在正文上:只在判断词下面画提醒色波浪线,那段文字是一个可点的片段,句末挂「说法待核」,n / N 跳得到;点开写种类的叫法(同审计)、服务端的原话、所挂的结论(点一下落到它的判定语)。正文后面是系统写的「分析范围」(现象、拆过、排除、算不了、没拆的候选维度、做过几次差异检验),编号点得开——正文没引用的取新接口 …/evidence/analyses/{alias} 的出处链,关掉焦点回到那个编号。整句面板挂的依据是分析结论时写「派生自 Q1、Q2」(证据图的 derived_from 边,按这份报告认)。记录页:审计表的说法违规行写种类的叫法、点得开;工件页签认得分析结论(哪一种分析,算不了的写没有做成)。夹具 evidence-analysis.json 是真运行组装的(后端测试库里的虚构零售库 shop,模型换成脚本)。check-evidence 加三节(分析结论的出处链、说法违规与分析范围、指标上的两期可比性),check-runs 加一节(审计行、工件页签)
This commit is contained in:
@@ -41,6 +41,10 @@ const JUDGE_SETS = [fxj.formal, fxj.explore, fxj.plain, fxv.formal, fxv.explore]
|
||||
// Excel 导入期 4:推断的来源。文档由 compose_doc 真跑(带文档标记),片段答复由 _chain 真跑,推断来源的答复按契约
|
||||
// (backend/app/data/provenance_types.py)拼;cases 写明每个场景是哪个片段、片段文字、覆盖 P4-SPEC 4.4 的哪几条
|
||||
const fxp = JSON.parse(readFileSync(`${root}frontend/src/run/__tests__/evidence-provenance.json`, 'utf8'))
|
||||
// 分析能力一轮:分析结论(判定语、具名的数、结果表的格)、说法违规、系统写的分析范围、口径卡指标上的两期可比性。文档、片段接口、
|
||||
// 证据图、审计、分析范围每个编号的出处链(GET …/evidence/analyses/{alias})都是真后端答的(/tmp/agentlab-fix/w2c 的录制脚本)
|
||||
const fxa = JSON.parse(readFileSync(`${root}frontend/src/run/__tests__/evidence-analysis.json`, 'utf8'))
|
||||
const FXA = [fxa.agent, fxa.card, fxa.metric]
|
||||
/** 推断来源夹具里各期导入清单的工件 id:夹具只收了两份清单,其余的按「工件不存在」答,不落到真服务端 */
|
||||
const FXP_MANIFESTS = new Set(Object.values(fxp.provenance).flatMap((a) => a.version?.parts ?? []).map((p) => p.manifest))
|
||||
/** 夹具里某段文字是哪个片段:检查按文字认片段,重新生成夹具时编号变了也不用改 */
|
||||
@@ -553,6 +557,233 @@ await section('artifacts', '记录页的工件:口径卡指标集、报告文
|
||||
check('两种都算证据(列表里醒目)', got.evidence)
|
||||
})
|
||||
|
||||
/** 分析夹具的三次运行:片段、证据图、分析范围编号的出处链按运行 id 答(排在 routed 前面,别的交回 routed) */
|
||||
async function analysisRoutes(page, hits, graphOf = (set) => set.graph) {
|
||||
await page.route((u) => FXA.some((x) => new URL(u).pathname.includes(`/runs/${x.run_id}`)), (r) => {
|
||||
const url = new URL(r.request().url())
|
||||
const set = FXA.find((x) => url.pathname.includes(`/runs/${x.run_id}`))
|
||||
const seg = url.pathname.match(/\/evidence\/segments\/([^/]+)$/)
|
||||
if (seg) {
|
||||
const id = decodeURIComponent(seg[1])
|
||||
hits.push(`seg:${id}`)
|
||||
const body = set.segments[id]
|
||||
return body ? r.fulfill({ json: body }) : r.fulfill({ status: 404, json: { detail: '报告中没有这个片段', code: 'evidence_segment_not_found' } })
|
||||
}
|
||||
const an = url.pathname.match(/\/evidence\/analyses\/([^/]+)$/)
|
||||
if (an) {
|
||||
const alias = decodeURIComponent(an[1])
|
||||
hits.push(`analysis:${alias}`)
|
||||
const body = set.analyses[alias]
|
||||
return body ? r.fulfill({ json: body })
|
||||
: r.fulfill({ status: 404, json: { detail: '这份报告的证据目录里没有这份分析结论', code: 'evidence_analysis_not_found' } })
|
||||
}
|
||||
if (/\/evidence$/.test(url.pathname)) return r.fulfill({ json: graphOf(set) })
|
||||
if (url.pathname.endsWith(`/runs/${set.run_id}`)) return r.fulfill({ json: { id: set.run_id, run_class: 'exploratory', status: 'succeeded' } })
|
||||
return r.fulfill({ status: 404, json: { detail: '检查脚本没有这一项' } })
|
||||
})
|
||||
}
|
||||
|
||||
await section('analysis', '分析结论:判定语、具名的数、结果表的格的出处链(判定 → 方法 → 计算 → 输入 → 复算 → 封存)', async () => {
|
||||
const hits = []
|
||||
const { ctx, page, errors } = await open('/ui-harness.html?evidence=1', { before: (p) => analysisRoutes(p, hits) })
|
||||
const box = page.locator('#evidence-analysis')
|
||||
const A = fxa.agent
|
||||
const phrase = box.locator('[data-seg="s3"]')
|
||||
check('判定语是一个可点的片段,画「有出处」', await phrase.count() === 1 && (await phrase.getAttribute('data-ev-state')) === 'deterministic')
|
||||
check('……读屏写出处:分析结论 A1 · 判定语', (await phrase.getAttribute('aria-label') ?? '').includes('有出处:分析结论 A1 · 判定语'),
|
||||
await phrase.getAttribute('aria-label'))
|
||||
await phrase.click()
|
||||
const p = panel(page)
|
||||
await p.locator('[data-ev-analysis="A1"]').waitFor({ timeout: 8000 })
|
||||
const judged = p.locator('[data-ev-analysis="A1"]')
|
||||
const jt = (await judged.innerText()).replace(/\s+/g, ' ')
|
||||
check('判定一步:标题写编号和方法的叫法(服务端的 method_label)', jt.includes('分析结论 A1 · 变化拆解(可加)'), jt.slice(0, 60))
|
||||
check('……判定语照原样、判定标准、点名的段、方向', (await judged.locator('[data-ev-analysis-phrase]').innerText()) === A.segments.s3.chain[0].phrase
|
||||
&& jt.includes('一段占总变化的一半以上算主要来源') && jt.includes('华东') && jt.includes('下降'), jt.slice(0, 200))
|
||||
check('……谁做的:Agent 现场做的分析、哪个节点', jt.includes('Agent 现场做的分析') && jt.includes('问数据'))
|
||||
check('……附带说明、Agent 写的假设(另写一句它不是结论)', jt.includes('没能核对本期的数据是否到齐')
|
||||
&& (await judged.locator('[data-ev-analysis-hypothesis]').innerText()).includes('下降是不是集中在某个地区')
|
||||
&& jt.includes('不是分析结论'), jt.slice(-200))
|
||||
check('……复算一致(服务端复算的那一句)、封存范围内的台账记过', await judged.locator('[data-ev-analysis-recompute="ok"]').count() === 1
|
||||
&& jt.includes('复算一致:按封存的输入重新计算,结论逐项相同') && await judged.locator('[data-ev-analysis-seal="ledger"]').count() === 1)
|
||||
check('……这一处引用的是判定语', (await judged.locator('[data-ev-analysis-cited]').getAttribute('data-ev-analysis-cited')) === 'phrase')
|
||||
const method = (await p.locator('[data-ev-analysis-method]').innerText()).replace(/\s+/g, ' ')
|
||||
check('方法一步:方法与版本、实际用的阈值(契约的叫法)、规范化后的参数', method.includes('变化拆解(可加) · decompose.additive@1')
|
||||
&& method.includes('「主要来源」的占比 0.5') && (await p.locator('[data-ev-analysis-params]').innerText()).includes('"by"'), method.slice(0, 160))
|
||||
const calc = p.locator('[data-ev-analysis-calc]')
|
||||
check('计算一步:关键数的代入式照服务端', (await calc.locator('[data-ev-analysis-formulas]').innerText()).includes('华东的占比 = 华东的变化 ÷ 总变化 = -300 ÷ (-340) = 88.2%'))
|
||||
check('……具名的数、结果表(表头是给人看的列名)', await calc.locator('[data-ev-analysis-value="top_share"]').count() === 1
|
||||
&& (await calc.locator('[data-ev-analysis-table] th').allInnerTexts()).join('、').includes('占变化的比重'))
|
||||
const inputs = p.locator('[data-ev-analysis-input]')
|
||||
check('输入一步:每份一节(基期总数、本期总数、数据)', JSON.stringify(await inputs.evaluateAll((els) => els.map((e) => e.dataset.evAnalysisInput)))
|
||||
=== 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)
|
||||
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()
|
||||
await page.waitForTimeout(150)
|
||||
check('……点输入里的 Q2:焦点跳到那个查询步骤', await page.evaluate(() => document.activeElement?.closest('[data-ev-query-alias]')?.getAttribute('data-ev-query-alias')) === 'Q2')
|
||||
check('分析结论的数不走口径账本:没有「口径」一步', await p.locator('[data-ev-ledger]').count() === 0)
|
||||
check('面板底下写服务端的说明(判定只说明变化来自哪里)', (await p.locator('[data-ev-analysis-note]').innerText()).includes('判定只说明变化来自哪里'))
|
||||
|
||||
// 具名的数、结果表的格:同一条链,「这一处引用的是」写清是哪一项,被引用的那个描出来
|
||||
await box.locator('[data-seg="s9"]').click()
|
||||
await p.locator('[data-ev-analysis-cited="value"]').waitFor({ timeout: 8000 })
|
||||
check('具名的数(88.2%):这一处引用的是具名的数「top_share」,那个数在计算里描出来', (await p.locator('[data-ev-analysis-cited]').innerText()) === '具名的数「top_share」'
|
||||
&& 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」'
|
||||
&& 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('片段接口按片段 id 取(s3、s9、s7)', ['seg:s3', 'seg:s9', 'seg:s7'].every((h) => hits.includes(h)), hits.join(','))
|
||||
|
||||
// 分析卡的结论:谁做的写分析卡(模板定好的分析);表格里引用结果表的格
|
||||
const card = page.locator('#evidence-analysis-card')
|
||||
await card.locator('[data-seg="s3"]').click()
|
||||
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('分析卡(模板定好的分析)'))
|
||||
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
|
||||
&& (await p.locator('[data-ev-analysis="A1"]').innerText()).includes('只有 0 期历史,至少要 8 期'))
|
||||
|
||||
// 窄栏:分析的几步栏内展开,不出横向滚动
|
||||
const narrow = page.locator('#evidence-analysis-narrow')
|
||||
await narrow.locator('[data-seg="s3"]').click()
|
||||
await page.waitForTimeout(400)
|
||||
const over = await narrow.evaluate((el) => el.scrollWidth - el.clientWidth)
|
||||
check('360 宽:分析结论的出处链栏内展开、不出横向滚动', over <= 1 && await narrow.locator('[data-ev-analysis-calc]').count() === 1, `${over}px`)
|
||||
if (SHOTS) {
|
||||
for (const theme of ['dark', 'light']) {
|
||||
await page.evaluate((t) => document.documentElement.setAttribute('data-theme', t), theme)
|
||||
await page.waitForTimeout(150)
|
||||
await box.locator('[data-seg="s3"]').click()
|
||||
await page.waitForTimeout(300)
|
||||
await page.screenshot({ path: `${SHOTS}/evidence-analysis-${theme}.png` })
|
||||
}
|
||||
}
|
||||
check('没有运行时报错', errors.length === 0, errors.join(' | '))
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('analysis-claims', '说法违规标在正文上、点开看原话和所挂的结论;n 跳得到;系统写的分析范围编号点得开;整句的依据写派生自谁', async () => {
|
||||
const hits = []
|
||||
const { ctx, page, errors } = await open('/ui-harness.html?evidence=1', { before: (p) => analysisRoutes(p, hits) })
|
||||
const box = page.locator('#evidence-analysis')
|
||||
const flagged = box.locator('[data-ev-state="claim"]')
|
||||
check('三句说法违规各是一个可点的片段', JSON.stringify(await flagged.evaluateAll((els) => els.map((e) => e.dataset.seg))) === '["s12","s14","s16"]')
|
||||
const words = await box.locator('[data-ev-claim-word]').allInnerTexts()
|
||||
check('……只在判断词下面画线(主要来自、异常、由于)', JSON.stringify(words) === '["主要来自","异常","由于"]', JSON.stringify(words))
|
||||
const deco = await box.locator('[data-ev-claim-word]').first().evaluate((el) => getComputedStyle(el).textDecorationStyle)
|
||||
check('……线型是波浪线(和无证据的点状线分得开)', deco === 'wavy', deco)
|
||||
check('……整段文字不画线', await flagged.first().evaluate((el) => getComputedStyle(el).textDecorationLine) === 'none')
|
||||
check('……句末挂「说法待核」', await box.locator('[data-ev-tag="claim"]').count() === 3)
|
||||
check('……读屏写种类', (await flagged.first().getAttribute('aria-label') ?? '').includes('说法待核:判断与分析结论不符'))
|
||||
await flagged.first().click()
|
||||
const p = panel(page)
|
||||
await p.locator('[data-ev-claim-flags]').waitFor({ timeout: 8000 })
|
||||
const flag = p.locator('[data-ev-claim-flag="analysis_claim_mismatch"]')
|
||||
const ft = (await flag.innerText()).replace(/\s+/g, ' ')
|
||||
check('点开:种类的叫法(同审计)、服务端的原话', ft.includes('判断与分析结论不符') && ft.includes('「主要来自华南」与所挂的分析结论 A1 对不上'), ft)
|
||||
await flag.locator('[data-ev-claim-analysis="A1"]').click()
|
||||
await page.waitForTimeout(400)
|
||||
check('……所挂的结论 A1 点一下:落到正文里 A1 的判定语、打开它的出处链', await active(page) === 's3'
|
||||
&& await p.locator('[data-ev-analysis="A1"]').count() === 1, await active(page))
|
||||
await box.locator('[data-seg="s14"]').click()
|
||||
await p.locator('[data-ev-claim-flag="analysis_claim_uncited"]').waitFor({ timeout: 8000 })
|
||||
check('判断词没挂分析结论:写这句没有挂分析结论', (await p.locator('[data-ev-claim-flag="analysis_claim_uncited"]').innerText()).includes('这句没有挂分析结论'))
|
||||
await box.locator('[data-seg="s16"]').click()
|
||||
await p.locator('[data-ev-claim-flag="causal_unsupported"]').waitFor({ timeout: 8000 })
|
||||
check('因果说法没有依据:原话照服务端', (await p.locator('[data-ev-claim-flag="causal_unsupported"]').innerText()).includes('是原因判断'))
|
||||
// n:从头跳,第一处就是说法违规
|
||||
await box.locator('[data-seg="s3"]').focus()
|
||||
await page.keyboard.press('n')
|
||||
await page.waitForTimeout(150)
|
||||
check('n 跳到下一处有问题的地方:说法违规那一段', ['s12', 's14', 's16'].includes(await active(page)), await active(page))
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// 违规清单:三条说法违规的叫法、能定位
|
||||
await page.evaluate(() => document.querySelector('#evidence-analysis [data-evidence-list]')?.click())
|
||||
await p.locator('[data-ev-violations]').waitFor({ timeout: 8000 })
|
||||
const vt = (await p.locator('[data-ev-violations]').innerText()).replace(/\s+/g, ' ')
|
||||
check('违规清单:三条都写种类的叫法、都能定位', ['判断与分析结论不符', '判断词未挂分析结论', '因果说法没有依据'].every((t) => vt.includes(t))
|
||||
&& await p.locator('[data-ev-violation][data-ev-locatable="yes"]').count() === 3, vt.slice(0, 160))
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// 分析范围:系统写的一段,编号点得开(正文没引用的 A2、A3 也点得开)
|
||||
const scope = box.locator('[data-ev-scope]')
|
||||
const st = (await scope.innerText()).replace(/\s+/g, ' ')
|
||||
check('正文后面是系统写的「分析范围」:现象、拆过(按地区)、算不了、没拆(渠道)', st.includes('分析范围') && st.includes('系统按这次的分析结论写的')
|
||||
&& JSON.stringify(await scope.locator('[data-ev-scope-group]').evaluateAll((els) => els.map((e) => e.dataset.evScopeGroup)))
|
||||
=== '["phenomena","decomposed","refused","not_decomposed"]' && st.includes('按地区') && st.includes('候选维度里没拆过的:渠道'), st.slice(0, 200))
|
||||
await scope.locator('[data-seg="scope:A2"]').click()
|
||||
await p.locator('[data-ev-analysis="A2"]').waitFor({ timeout: 8000 })
|
||||
check('点范围里的 A2(正文没引用):取它的出处链(…/evidence/analyses/A2),面板写判定和算不了的原因', hits.includes('analysis:A2')
|
||||
&& await p.locator('[data-ev-analysis-refused]').count() === 1 && (await p.locator('[data-ev-analysis="A2"]').innerText()).includes('至少要 8 期'),
|
||||
hits.join(','))
|
||||
check('……不画所在的句子(它不在正文里)', await p.locator('[data-ev-sentence]').count() === 0)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
check('……Esc 关掉:焦点回到范围里的那个编号', await page.evaluate(() => document.activeElement?.getAttribute('data-seg')) === 'scope:A2')
|
||||
await scope.locator('[data-seg="scope:A1"]').click()
|
||||
await p.locator('[data-ev-analysis="A1"]').waitFor({ timeout: 8000 })
|
||||
check('点范围里的 A1:同一条链(方法、计算、输入都在)', await p.locator('[data-ev-analysis-method]').count() === 1
|
||||
&& await p.locator('[data-ev-analysis-input]').count() === 3)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// 审计行:说法违规的那几行带问题代码(记录页的审计表按它写种类的叫法,不写「无证据」)
|
||||
const audit = await page.evaluate(async (a) => (await import('/src/lib/evidence.ts')).auditFromApi(a)
|
||||
.filter((r) => r.kind === 'violation').map((r) => r.issue), fxa.agent.audit)
|
||||
check('审计:说法违规的行带问题代码', JSON.stringify(audit) === '["analysis_claim_mismatch","analysis_claim_uncited","causal_unsupported"]', JSON.stringify(audit))
|
||||
check('没有运行时报错', errors.length === 0, errors.join(' | '))
|
||||
await ctx.close()
|
||||
|
||||
// 整句面板:挂的依据是分析结论时写它派生自哪几份查询(证据图的 derived_from 边)。这份报告没开裁判,句末没有徽标;
|
||||
// 证据图上加一条封存后的按需判定(探索运行点「判这一句」后就是这样),u1 句末才有徽标、点得开整句
|
||||
const judged = (set) => set !== fxa.agent ? set.graph : { ...set.graph, reports: set.graph.reports.map((r) => ({ ...r,
|
||||
post_seal_verdicts: { u1: { status: 'supported', rationale: '判定语照 A1 的判定写', judge: 'judge-model-b', post_seal: true, used: ['A1'] } } })) }
|
||||
const second = await open('/ui-harness.html?evidence=1', { before: (p) => analysisRoutes(p, [], judged) })
|
||||
const badge = second.page.locator('#evidence-analysis [data-ev-claim="u1"]')
|
||||
await badge.waitFor({ timeout: 8000 })
|
||||
await badge.click()
|
||||
const cites = panel(second.page).locator('[data-ev-cites]')
|
||||
await cites.waitFor({ timeout: 8000 })
|
||||
await second.page.waitForTimeout(300)
|
||||
const derived = cites.locator('[data-ev-cite="A1"] [data-ev-derived]')
|
||||
check('整句的依据是 A1:写「派生自 Q1、Q2」(证据图的 derived_from 边,按这份报告认)', await derived.count() === 1
|
||||
&& (await derived.getAttribute('data-ev-derived')) === 'Q1,Q2' && (await derived.innerText()).includes('Q1、Q2'),
|
||||
(await cites.innerText()).replace(/\s+/g, ' '))
|
||||
check('……挂的依据里 A1 写分析结论的叫法', (await cites.locator('[data-ev-cite="A1"]').innerText()).includes('变化拆解'),
|
||||
(await cites.locator('[data-ev-cite="A1"]').innerText()).replace(/\s+/g, ' '))
|
||||
check('没有运行时报错(整句面板)', second.errors.length === 0, second.errors.join(' | '))
|
||||
await second.ctx.close()
|
||||
})
|
||||
|
||||
await section('analysis-metric', '口径卡指标上的两期可比性:判定、判定语,展开看那份可比性结论的整条链(随卡封存)', async () => {
|
||||
const hits = []
|
||||
const { ctx, page, errors } = await open('/ui-harness.html?evidence=1', { before: (p) => analysisRoutes(p, hits) })
|
||||
const box = page.locator('#evidence-analysis-metric')
|
||||
await box.locator('[data-seg="s1"]').click()
|
||||
const p = panel(page)
|
||||
await p.locator('[data-ev-comparability]').waitFor({ timeout: 8000 })
|
||||
const row = p.locator('[data-ev-comparability]')
|
||||
check('指标一步里多一行「两期可比性」:判定写叫法、判定语照服务端', (await row.getAttribute('data-ev-comparability')) === 'comparable'
|
||||
&& (await row.innerText()).includes('两期可比') && (await row.locator('[data-ev-comparability-phrase]').innerText()) === '两期可比,窗口长度、整期、星期构成核对不了')
|
||||
await row.locator('[data-ev-comparability-toggle]').click()
|
||||
await page.waitForTimeout(200)
|
||||
const chain = row.locator('[data-ev-comparability-chain]')
|
||||
check('展开:那份可比性结论的判定、方法、计算、输入', await chain.locator('[data-ev-analysis-origin="metric"]').count() === 1
|
||||
&& await chain.locator('[data-ev-analysis-method]').count() === 1 && await chain.locator('[data-ev-analysis-input]').count() === 2)
|
||||
check('……谁做的写「口径卡执行时自动做的可比性检查」,封存写随卡封存(不进台账)', (await chain.innerText()).includes('口径卡执行时自动做的可比性检查')
|
||||
&& await chain.locator('[data-ev-analysis-seal="card"]').count() === 1)
|
||||
check('……复算一致', await chain.locator('[data-ev-analysis-recompute="ok"]').count() === 1)
|
||||
check('没有运行时报错', errors.length === 0, errors.join(' | '))
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('long', '长报告:块级 content-visibility、折叠、跳进折叠区自动展开', async () => {
|
||||
const box = '#evidence-long'
|
||||
const info = await page.evaluate((sel) => {
|
||||
|
||||
@@ -2244,6 +2244,59 @@ await section('证据页签:裁判拆档——证据相矛盾、证据不足
|
||||
}
|
||||
})
|
||||
|
||||
await section('证据页签:分析说法违规的审计行写种类的叫法;工件页签认得分析结论(分析能力一轮)', async () => {
|
||||
// 夹具是后端真跑的(evidence-analysis.json 的 agent:Agent 做了变化拆解、波动基线(算不了)、可比性检查,报告有三处说法违规)。
|
||||
// 审计行由 api/evidence.py 给出:说法违规列在「无证据」一组,issue 是 analysis_claim_mismatch 这几种
|
||||
const fxa = JSON.parse(readFileSync(new URL('../frontend/src/run/__tests__/evidence-analysis.json', import.meta.url), 'utf8'))
|
||||
const a = fxa.agent
|
||||
const EV_A = 'fake0evanalysis00000000000000'
|
||||
const output = { 周报: a.doc.markdown, _evidence: { report_node: 'write', doc_artifact: a.doc_artifact, fields: ['周报'] }, _issuance: a.issuance }
|
||||
evFakes(EV_A, { output, graph: a.graph, audit: a.audit })
|
||||
fakes.set(`GET /api/artifacts/${a.doc_artifact}`, () => ({ status: 200, json: { id: a.doc_artifact, content: a.doc } }))
|
||||
const results = Object.values(a.doc.catalog).filter((e) => e?.kind === 'analysis')
|
||||
const at = new Date(Date.now() - 7198e3).toISOString()
|
||||
fakes.set(`GET /api/runs/${EV_A}/artifacts`, () => ({ status: 200, json: results.map((e) => ({ id: e.artifact, kind: 'analysis_result',
|
||||
node_id: 'fetch', size: 1800, meta: { op: a.analyses[e.alias].chain[0].op, status: a.analyses[e.alias].chain[0].status }, created_at: at })) }))
|
||||
try {
|
||||
await openEvidence(EV_A)
|
||||
await page.locator('[data-audit-table]').waitFor({ timeout: 5000 }).catch(() => {})
|
||||
const rows = await page.locator('[data-audit-table] tr[data-audit-row]').evaluateAll((els) => els.map((e) => ({
|
||||
issue: e.querySelector('[data-audit-issue]')?.getAttribute('data-audit-issue') ?? '', state: e.children[1]?.textContent ?? '',
|
||||
label: e.querySelector('[data-audit-focus]')?.getAttribute('aria-label') ?? '' })))
|
||||
const claimed = rows.filter((r) => r.issue.startsWith('analysis_claim') || r.issue === 'causal_unsupported')
|
||||
check('审计表:三条说法违规各一行,状态写种类的叫法(同正文、面板),字形照「无证据」那一组,不写「无证据」',
|
||||
JSON.stringify(claimed.map((r) => r.state)) === '["?判断与分析结论不符","?判断词未挂分析结论","?因果说法没有依据"]', JSON.stringify(rows.map((r) => r.state)))
|
||||
check('……三行都点得开(正文里那段文字画着波浪线、点得开),读屏写种类', claimed.length === 3
|
||||
&& claimed.every((r) => r.label.includes(',') && !r.label.includes('无证据')), JSON.stringify(claimed.map((r) => r.label)))
|
||||
check('……别的无证据行照旧写「无证据」(没挂依据的两句结论)', rows.filter((r) => !r.issue && r.state === '?无证据').length === 2,
|
||||
JSON.stringify(rows.map((r) => `${r.issue}:${r.state}`)))
|
||||
check('……有出处那一组:判定语、具名的数、结果表的格三行', rows.filter((r) => r.state.includes('有出处')).length === 3, JSON.stringify(rows.map((r) => r.state)))
|
||||
await page.route(new RegExp(`/api/runs/${EV_A}/evidence/segments/`), (r) => {
|
||||
const sid = decodeURIComponent(new URL(r.request().url()).pathname.split('/').pop())
|
||||
return a.segments[sid] ? r.fulfill({ json: a.segments[sid] }) : r.fulfill({ status: 404, json: { detail: '没有这个片段' } })
|
||||
})
|
||||
await page.locator('[data-audit-table] [data-audit-open="s12"]').click()
|
||||
const dock = page.locator('[data-evidence-dock] [data-evidence-panel]')
|
||||
await dock.locator('[data-ev-claim-flags]').waitFor({ timeout: 5000 }).catch(() => {})
|
||||
check('点说法违规那一行:右边打开那段文字,写种类和服务端的原话', (await dock.locator('[data-ev-claim-flag="analysis_claim_mismatch"]').innerText()
|
||||
.catch(() => '')).includes('与所挂的分析结论 A1 对不上'))
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
await page.goto(`${WEB}/runs/${EV_A}?view=artifacts`, { waitUntil: 'networkidle' })
|
||||
await page.locator('[data-run-artifacts] [data-artifact-id]').first().waitFor({ timeout: 5000 }).catch(() => {})
|
||||
const shown = await page.locator('[data-run-artifacts] [data-artifact-id]').evaluateAll((els) => els.map((e) => ({
|
||||
kind: e.querySelector('[data-artifact-kind-label]')?.textContent ?? '', about: e.querySelector('[data-artifact-about]')?.textContent ?? '',
|
||||
group: e.getAttribute('data-artifact-class') })))
|
||||
check('工件页签:分析结论写种类的叫法、哪一种分析,算不了的写「没有做成」,归在证据快照一组',
|
||||
JSON.stringify(shown) === JSON.stringify([{ kind: '分析结论', about: '变化拆解', group: 'evidence' },
|
||||
{ kind: '分析结论', about: '波动基线(没有做成)', group: 'evidence' }, { kind: '分析结论', about: '可比性检查', group: 'evidence' }]),
|
||||
JSON.stringify(shown))
|
||||
} finally {
|
||||
for (const k of [...fakes.keys()]) if (k.includes(EV_A) || k.includes(a.doc_artifact)) fakes.delete(k)
|
||||
await page.unroute(new RegExp(`/api/runs/${EV_A}/evidence/segments/`)).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
await section('证据页签:没有报告的运行照实说明(none / 旧运行猜测 / 老后端)', async () => {
|
||||
evFakes(EV_LEGACY, { output: fxe.legacy.output, graph: fxe.legacy.graph, audit: fxe.legacy.audit, report: false })
|
||||
await openEvidence(EV_LEGACY)
|
||||
|
||||
Reference in New Issue
Block a user