mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:41:58 +00:00
出具缺口里核查报上来的另起一组「核查未通过」,时间线认得期 3 留下的三个核查日志代码(前端)
- 时间线的出具那一行、成果节点卡片的出具印章、证据页的出具横幅:双盲复算、反方审稿报上来的缺口(「双盲复算「节点名」:」「反方审稿:」开头,按节点类型的前缀认,名字里带这几个字的别的节点不算)归在「核查未通过」下,不再和「校验不完整」混在一起——它们是核查查出来的问题(两路结果不一致、结论被推翻、指标无法复算),不是校验没做完。分组只有 run/decode.ts 的 splitIssuanceGaps 一处,三个地方共用 - 时间线解读 dual_check_unchecked(哪几个指标无法复算、各自的原因)、challenge_unreviewed(几句没能反查、第一句的原因)、challenge_sources_lost(几句只查了部分数据源、没查成的源):以前认不得,只能照原话显示;记入数据目录那一行带上写进去的值,写法同审批卡的预告 - 反方审稿的时间线文案从 DUAL_CHECK_TEXT.challengeLog 挪到 CHALLENGE_TEXT.log(各节点的文案放在各自的块里),decode.ts 跟着改引用 - 工件种类叫法补上 dual_check(双盲复算对照)、challenge(反方审稿记录),工件列表不再显示键名 - check-decode 补一节:三个日志代码、出具缺口的分组和顺序、前缀不重复节点类型、缺字段的老日志不崩
This commit is contained in:
@@ -10,7 +10,7 @@ import { NODE_DEFS, sourceHandles } from './nodeDefs'
|
||||
import { DENSE_EXITS, NODE_WIDTH } from './routing'
|
||||
import { LiveClock, stillClock, TeamMatrix, teamBrief, type TeamEnding } from './TeamMatrix'
|
||||
import { edgeKey, topology } from '../run/derive'
|
||||
import { exhaustedOf } from '../run/decode'
|
||||
import { exhaustedOf, splitIssuanceGaps } from '../run/decode'
|
||||
import { api } from '../api/client'
|
||||
import { isComposing, StatusBadge, toast } from '../components/ui'
|
||||
import { claimCountsOf, matchedSource } from '../lib/evidence'
|
||||
@@ -898,10 +898,12 @@ function NodeCardImpl({ id, data, selected, isConnectable }: NodeProps<FlowNode>
|
||||
const tier = view.at != null && issuedAt != null && view.at < issuedAt ? undefined : issuance?.tier
|
||||
const tierClass = tier === 'formal' ? 'is-formal' : tier === 'degraded' ? 'is-degraded' : tier === 'withheld' ? 'is-withheld' : ''
|
||||
const detail = issuanceDetail?.tier ? issuanceDetail : undefined
|
||||
const gaps: string[] = Array.isArray(detail?.gaps) ? detail.gaps.map(String) : []
|
||||
// 双盲复算、反方审稿报上来的缺口是核查查出来的问题(两路不一致、结论被推翻),和「校验没跑全」分开说
|
||||
const { verify: verifyGaps, other: gaps } = splitIssuanceGaps(Array.isArray(detail?.gaps) ? detail.gaps.map(String) : [])
|
||||
const stampTitle = tier ? [
|
||||
`出具判定:${issuanceLabel(tier)}`,
|
||||
detail ? `可追溯 ${detail.matched_numbers ?? 0} 个数字 / 核对 ${detail.metrics_checked ?? 0} 个指标` : '',
|
||||
verifyGaps.length ? `${DUAL_CHECK_TEXT.verifyGaps.head}:${verifyGaps.join(';')}` : '',
|
||||
// 降档常常不是数字对不上,而是校验根本没跑全(叙述模板渲染为空、指标集为空):
|
||||
// 不写出来,悬停在一个「降档出具」上看不到任何理由
|
||||
gaps.length ? `校验未全部完成:${gaps.join(';')}` : '',
|
||||
|
||||
+44
-20
@@ -3335,7 +3335,9 @@ export const WHATIF_TEXT = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 双盲复算:节点库说明、检查器、节点卡片摘要、两边对不上时的审批卡,以及时间线上双盲复算和反方审稿的日志。
|
||||
* 双盲复算:节点库说明、检查器、节点卡片摘要、两边对不上时的审批卡,以及时间线上双盲复算的日志。核查的上限叫法
|
||||
* (limitLabel)和出具缺口里「核查未通过」那一组(verifyGaps)反方审稿也用,放在这里一处;反方审稿自己的时间线说法在
|
||||
* CHALLENGE_TEXT.log。
|
||||
*
|
||||
* 两边的叫法固定为「原流程」「盲算」(服务端 explain_sql_diff 的 labels 也是这两个词),审核人的三个选择固定为
|
||||
* 「采用原流程的结果」「采用盲算的结果」「两边都不对」。选「采用盲算的结果」不会替换报告里的数,只记缺口,
|
||||
@@ -3570,31 +3572,24 @@ export const DUAL_CHECK_TEXT = {
|
||||
const where = table ? `「${table}」${source ? `(${source})` : ''}` : ''
|
||||
return where && item ? `${where}的${item}` : where || item || ''
|
||||
},
|
||||
/** 写进数据目录的一项连同写进去的值:「orders」(shop)的业务日期记为「paid_date」 */
|
||||
catalogValue: (where: string, value: string) => `${where}记为「${value}」`,
|
||||
catalogSkipped: (n: number) => `${formatNumber(n)} 项未写入`,
|
||||
catalogNext: '可在「数据 → 数据目录」中查看或修改',
|
||||
/** metric 是指标 ID(服务端日志只带 ID,中文名在原话里),不当名字加引号 */
|
||||
limit: (label: string, metric?: string) => (metric ? `指标 ${metric} 的盲算已达${label},按无法复算处理` : `双盲复算已达${label}`),
|
||||
limitNext: '请在「设置 → 偏好设置 → 核查」中调高双盲复算的上限',
|
||||
/** dual_check_unchecked:盲算触顶、出错、交不出数,或者输入本身没法单独复算(合并查询、遮罩列) */
|
||||
unchecked: (n?: number) => (n ? `双盲复算有 ${formatNumber(n)} 个指标无法复算,按缺口计入出具`
|
||||
: '双盲复算有指标无法复算,按缺口计入出具'),
|
||||
uncheckedNext: '无法复算的原因见展开内容。来自合并查询、遮罩列的输入无法单独复算;盲算触顶、出错的,可调高「设置 → 偏好设置 → 核查」中的上限后重新运行',
|
||||
},
|
||||
/** 反方审稿日志的时间线说法:时间线解读统一在这里做,反方审稿的工作包不另写一份 */
|
||||
challengeLog: {
|
||||
skippedDisabled: '反方审稿未启用,已跳过',
|
||||
skippedDisabledNext: '如需审稿,请在节点中开启,或在「设置 → 偏好设置 → 核查」中启用反方审稿',
|
||||
// no_report:要审的报告节点找得到(找不到时节点直接报错,到不了这里),但它的报告文档读不出来——没有写出、
|
||||
// 保存失败,或者它在这次没有执行到的分支上
|
||||
skippedNoReport: '要审的报告没有可读取的报告文档,已跳过反方审稿',
|
||||
skippedNoReportNext: '请确认报告撰写节点是否执行成功、是否写出了报告;它位于条件分支上时,确认本次运行执行到了它',
|
||||
skippedNoClaims: '报告中没有需要审的关键结论,已跳过反方审稿',
|
||||
skipped: '反方审稿已跳过',
|
||||
sameModel: (model?: string) => (model ? `反方审稿模型与报告撰写模型同为「${model}」,审稿缺乏独立性`
|
||||
: '反方审稿模型与报告撰写模型相同,审稿缺乏独立性'),
|
||||
sameModelNext: '请在节点中指定其他审稿模型,或在「设置 → 偏好设置 → 核查」中修改核查模型',
|
||||
done: (n?: number) => (n ? `反方审稿完成:审阅了 ${formatNumber(n)} 句结论` : '反方审稿完成'),
|
||||
verdict: { stands: '经受住', weakened: '被削弱', refuted: '被推翻', inconclusive: '无法判断' } as Record<string, string>,
|
||||
refuted: (n?: number) => (n ? `反方审稿推翻了 ${formatNumber(n)} 句结论` : '反方审稿推翻了报告中的结论'),
|
||||
refutedNext: '请在证据中对照反方审稿的查询和理由,核实这些结论',
|
||||
limit: (label: string, skipped?: number) => (skipped ? `反方审稿已达${label},${formatNumber(skipped)} 句结论未审` : `反方审稿已达${label}`),
|
||||
limitNext: '请在节点或「设置 → 偏好设置 → 核查」中调高反方审稿的上限',
|
||||
/**
|
||||
* 出具缺口里双盲复算、反方审稿报上来的那几条(缺口以「双盲复算「节点名」:」「反方审稿:」开头):时间线、成果卡片、
|
||||
* 证据页把它们另起一组,不和「校验没跑全」混在一起——它们是核查查出来的问题,不是校验没做完
|
||||
*/
|
||||
verifyGaps: {
|
||||
head: '核查未通过',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3703,6 +3698,35 @@ export const CHALLENGE_TEXT = {
|
||||
truncated: '结果已截断,数据库中还有更多数据',
|
||||
/** 理由里有出处的数字:读屏听到的是「数字,出处:查询 Q2 · 第 1 行 · amount」,点一下跳到那次查询 */
|
||||
cited: (text: string, source: string) => `${text},出处:${source},跳到这次查询`,
|
||||
|
||||
// ---- 时间线(run/decode.ts explainLog):按日志的 code 和结构化字段说,不从原话里认
|
||||
log: {
|
||||
skippedDisabled: '反方审稿未启用,已跳过',
|
||||
skippedDisabledNext: '如需审稿,请在节点中开启,或在「设置 → 偏好设置 → 核查」中启用反方审稿',
|
||||
// no_report:要审的报告节点找得到(找不到时节点直接报错,到不了这里),但它的报告文档读不出来——没有写出、
|
||||
// 保存失败,或者它在这次没有执行到的分支上
|
||||
skippedNoReport: '要审的报告没有可读取的报告文档,已跳过反方审稿',
|
||||
skippedNoReportNext: '请确认报告撰写节点是否执行成功、是否写出了报告;它位于条件分支上时,确认本次运行执行到了它',
|
||||
skippedNoClaims: '报告中没有需要审的关键结论,已跳过反方审稿',
|
||||
skipped: '反方审稿已跳过',
|
||||
sameModel: (model?: string) => (model ? `反方审稿模型与报告撰写模型同为「${model}」,审稿缺乏独立性`
|
||||
: '反方审稿模型与报告撰写模型相同,审稿缺乏独立性'),
|
||||
sameModelNext: '请在节点中指定其他审稿模型,或在「设置 → 偏好设置 → 核查」中修改核查模型',
|
||||
done: (n?: number) => (n ? `反方审稿完成:审阅了 ${formatNumber(n)} 句结论` : '反方审稿完成'),
|
||||
verdict: { stands: '经受住', weakened: '被削弱', refuted: '被推翻', inconclusive: '无法判断' } as Record<string, string>,
|
||||
refuted: (n?: number) => (n ? `反方审稿推翻了 ${formatNumber(n)} 句结论` : '反方审稿推翻了报告中的结论'),
|
||||
refutedNext: '请在证据中对照反方审稿的查询和理由,核实这些结论',
|
||||
limit: (label: string, skipped?: number) => (skipped ? `反方审稿已达${label},${formatNumber(skipped)} 句结论未审` : `反方审稿已达${label}`),
|
||||
limitNext: '请在节点或「设置 → 偏好设置 → 核查」中调高反方审稿的上限',
|
||||
/** challenge_unreviewed:有句子没能反查(核查模型不可用、数据源不在了……),记为无法判断 */
|
||||
unreviewed: (n?: number) => (n ? `反方审稿有 ${formatNumber(n)} 句结论没能反查,已记为无法判断`
|
||||
: '反方审稿有结论没能反查,已记为无法判断'),
|
||||
unreviewedNext: '这几句没有真正查询数据。请检查核查模型和数据源是否可用,或在「设置 → 偏好设置 → 核查」中调高上限后重新运行',
|
||||
/** challenge_sources_lost:反查了,但该查的数据源有停用了的,判断不含它们的数据 */
|
||||
sourcesLost: (n?: number) => (n ? `反方审稿有 ${formatNumber(n)} 句结论只查了部分数据源` : '反方审稿有结论只查了部分数据源'),
|
||||
sourcesLostSub: (names: string[]) => `未能查询:${names.map((x) => `「${x}」`).join('、')}(已不存在或已停用),判断不含其中的数据`,
|
||||
sourcesLostNext: '请在「数据」页确认这些数据源是否应启用,启用后重新运行',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
issuanceMarks, segmentState, unitVerdict, verdictState, type AuditFilter, type AuditGroup, type AuditRow,
|
||||
} from '../../lib/evidence'
|
||||
import { formatNumber } from '../../lib/format'
|
||||
import { EVIDENCE_AUDIT_TEXT as T, UPGRADE_TEXT } from '../../lib/terms'
|
||||
import { DUAL_CHECK_TEXT, EVIDENCE_AUDIT_TEXT as T, UPGRADE_TEXT } from '../../lib/terms'
|
||||
import { IssuanceBanner } from '../../run/AssistantStream'
|
||||
import { splitIssuanceGaps } from '../../run/decode'
|
||||
import { EvidenceDoc, type EvidenceDocHandle } from '../../run/EvidenceDoc'
|
||||
import { EvidenceGuessView } from '../../run/EvidenceGuess'
|
||||
import { Markdown, inlineStyles } from '../../run/Markdown'
|
||||
@@ -145,11 +146,7 @@ export function EvidencePane({ run, output, labelOf, refreshKey }: {
|
||||
<UpgradeBanner run={run} mode={mode} />
|
||||
{/* 出具档位和降档原因写在报告上方:降档时只在侧栏小标签里写「SQL 检查未通过」,报告上却是绿色的
|
||||
「10 个数字都有出处」,读的人会以为一切正常。SQL 检查导致的降档在横幅里单列一块 */}
|
||||
{!!output?._issuance?.tier && (
|
||||
<div data-evidence-issuance="">
|
||||
<IssuanceBanner issuance={output._issuance} runClass={run.run_class} />
|
||||
</div>
|
||||
)}
|
||||
{!!output?._issuance?.tier && <IssuanceWithChecks issuance={output._issuance} runClass={run.run_class} />}
|
||||
|
||||
{mode === 'cited' && reports.map((r) => {
|
||||
const doc = docOf(r.node_id)
|
||||
@@ -314,6 +311,31 @@ function useAudit(runId: string, key: string): AuditState {
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* 出具横幅,外加双盲复算、反方审稿报上来的缺口另起的一组「核查未通过」。那几条是核查查出来的问题(两路结果不一致、
|
||||
* 结论被推翻、指标无法复算),横幅把缺口都列在「校验未全部完成」下,读起来像是校验没做完:这里从横幅里拿出来,
|
||||
* 紧跟在它下面单列,颜色和横幅里的缺口一样
|
||||
*/
|
||||
function IssuanceWithChecks({ issuance, runClass }: { issuance: Record<string, any>; runClass?: string }) {
|
||||
const { verify, other } = useMemo(
|
||||
() => splitIssuanceGaps(Array.isArray(issuance.gaps) ? issuance.gaps.map(String) : []), [issuance])
|
||||
const shown = useMemo(() => (verify.length ? { ...issuance, gaps: other } : issuance), [issuance, verify, other])
|
||||
return (
|
||||
<div className="space-y-1.5" data-evidence-issuance="">
|
||||
<IssuanceBanner issuance={shown} runClass={runClass} />
|
||||
{verify.length > 0 && (
|
||||
<div className="rounded-lg border px-2 py-1.5 text-2xs leading-relaxed"
|
||||
style={{ borderColor: 'var(--st-waiting)', color: 'var(--st-waiting)' }} data-evidence-verify-gaps="">
|
||||
<div className="font-medium">{DUAL_CHECK_TEXT.verifyGaps.head}</div>
|
||||
<ul className="mt-0.5 space-y-0.5 pl-2 [overflow-wrap:anywhere]">
|
||||
{verify.map((g) => <li key={g}>· {g}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 顶上一行:这次运行按哪种方式展示证据、封存核对的结果、每份报告文档的哈希 */
|
||||
function SummaryBar({ mode, seal, message, reports, labelOf }: {
|
||||
mode?: string
|
||||
|
||||
@@ -332,6 +332,8 @@ export const ARTIFACT_KIND_LABEL: Record<string, string> = {
|
||||
metric_set: '口径卡指标集',
|
||||
report_doc: '报告文档',
|
||||
schema_snapshot: '表结构快照',
|
||||
dual_check: '双盲复算对照',
|
||||
challenge: '反方审稿记录',
|
||||
}
|
||||
export const artifactKindLabel = (kind: string): string => ARTIFACT_KIND_LABEL[kind] ?? kind
|
||||
|
||||
@@ -349,6 +351,11 @@ export function artifactDescription(kind: string, meta?: Record<string, any> | n
|
||||
// 查库时冻结的表结构(工件行的 meta 里有 {source, tables: 表数量}):报告里的表名、字段名按它核对
|
||||
case 'schema_snapshot': return meta?.source
|
||||
? `表结构:数据源「${meta.source}」${typeof meta.tables === 'number' ? ` · ${meta.tables} 张表` : ''}` : '查询时保存的表结构'
|
||||
// 两路结果对不上时交给审核人的完整对照(工件行的 meta 里有 {metrics: 指标个数})
|
||||
case 'dual_check': return typeof meta?.metrics === 'number'
|
||||
? `双盲复算:${meta.metrics} 个指标两边的 SQL、取值与差异说明` : '双盲复算:两边的 SQL、取值与差异说明'
|
||||
// 反方审稿的完整记录(工件行的 meta 里有 {report: 审的报告节点})
|
||||
case 'challenge': return '反方审稿:每句结论的判断、理由和反方执行的查询'
|
||||
default: return '工件'
|
||||
}
|
||||
}
|
||||
|
||||
+65
-12
@@ -3,7 +3,7 @@ import type { NodeState, RunPhase } from './trace'
|
||||
import { formatDuration, formatNumber } from '../lib/format'
|
||||
import {
|
||||
ABSENCE_CLAIM_TEXT, CATALOG_DRIFT_TEXT, CATALOG_PATCH_TEXT, COPILOT_CONTEXT_TEXT, JUDGE_TEXT, MERGE_TEXT, RUN_SQL_CHECK_TEXT, SCHEMA_PARTIAL_TEXT, SQL_CHECK_TEXT, TYPE_LABEL,
|
||||
CATALOG_TABLE_FIELD_LABEL, DUAL_CHECK_TEXT,
|
||||
CATALOG_TABLE_FIELD_LABEL, CHALLENGE_TEXT, DUAL_CHECK_TEXT, NODE_TYPE_LABEL,
|
||||
claimTally,
|
||||
evidenceTally, issuanceLabel, nodeTypeLabel,
|
||||
} from '../lib/terms'
|
||||
@@ -717,16 +717,31 @@ function gateTitle(tool: string, verdict: 'allow' | 'escalate' | 'team', reason:
|
||||
|
||||
/**
|
||||
* 双盲复算、反方审稿的日志代码(期 3 前后端共用的约定)。这几条按 code 和结构化字段解读,不看原话;后端发成 info 的
|
||||
* (一致、裁决、记入数据目录、审稿完成)也上时间线,不然这两种节点就是一个跑了几十秒、什么都没说的黑盒
|
||||
* (一致、裁决、记入数据目录、审稿完成)也上时间线,不然这两种节点就是一个跑了几十秒、什么都没说的黑盒。
|
||||
* 无法复算(dual_check_unchecked)、没能反查(challenge_unreviewed)、只查了部分数据源(challenge_sources_lost)是
|
||||
* 期 4 补上的:以前认不得,只能照原话显示
|
||||
*/
|
||||
const VERIFY_LOG_CODES = new Set([
|
||||
'dual_check_skipped', 'dual_check_same_model', 'dual_check_agreed', 'dual_check_disagreed', 'dual_check_resolved',
|
||||
'dual_check_catalog', 'dual_check_limit',
|
||||
'dual_check_catalog', 'dual_check_limit', 'dual_check_unchecked',
|
||||
'challenge_skipped', 'challenge_same_model', 'challenge_done', 'challenge_refuted', 'challenge_limit',
|
||||
'challenge_unreviewed', 'challenge_sources_lost',
|
||||
])
|
||||
|
||||
const DC = DUAL_CHECK_TEXT
|
||||
const CL = DUAL_CHECK_TEXT.challengeLog
|
||||
const CL = CHALLENGE_TEXT.log
|
||||
|
||||
/**
|
||||
* 出具缺口里哪几条是双盲复算、反方审稿报上来的:出具契约给上游节点报的缺口加了「节点类型「节点名」:」的前缀
|
||||
* (nodes/io.py 的 _upstream_issuance_gaps,节点名就是类型叫法时只写「节点类型:」),按这两种节点类型的叫法认。
|
||||
* 它们是核查查出来的问题(两路结果不一致、结论被推翻、指标无法复算),时间线、成果卡片、证据页另起一组「核查未通过」,
|
||||
* 不和「校验没跑全」的缺口混在一起。其余的照旧
|
||||
*/
|
||||
const VERIFY_GAP_KINDS = [NODE_TYPE_LABEL.dual_check, NODE_TYPE_LABEL.challenge]
|
||||
export function splitIssuanceGaps(gaps: readonly string[]): { verify: string[]; other: string[] } {
|
||||
const isVerify = (g: string) => VERIFY_GAP_KINDS.some((kind) => g.startsWith(`${kind}「`) || g.startsWith(`${kind}:`))
|
||||
return { verify: gaps.filter(isVerify), other: gaps.filter((g) => !isVerify(g)) }
|
||||
}
|
||||
const isRecord = (v: unknown): v is Record<string, any> => !!v && typeof v === 'object' && !Array.isArray(v)
|
||||
/** 老日志、别的服务端版本可能给的是一句中文原因而不是代码:中文的照写,认不得的代码不往界面上放 */
|
||||
const cjkText = (v: unknown): string | undefined => (typeof v === 'string' && /[㐀-鿿]/.test(v) ? v : undefined)
|
||||
@@ -1034,16 +1049,18 @@ function explainLog(code: string | undefined, message: string, d: Record<string,
|
||||
return { title: DC.log.resolved(parts), sub: gaps != null ? DC.log.gaps(gaps) : undefined, level: warn ? 'warn' : 'info' }
|
||||
}
|
||||
case 'dual_check_catalog': {
|
||||
// written:[{source, table, item}] 写进数据目录的已确认事实;skipped:[{reason}] 没写的(已有已确认的不覆盖……)。
|
||||
// written:[{source, table, item, value}] 写进数据目录的已确认事实;skipped:[{reason}] 没写的(已有已确认的不覆盖……)。
|
||||
// item 是表级目录项的键(business_date / valid_filter / dedup),按数据目录页的叫法写;认不出的键不往界面上放,
|
||||
// 只写表;老版本给的一句中文照写
|
||||
// 只写表;老版本给的一句中文照写。value 是写进去的说法(期 4 起带),写法同审批卡上的「会记为」,两处一字不差
|
||||
const written = Array.isArray(d.written) ? d.written.filter(isRecord) : []
|
||||
const skipped = Array.isArray(d.skipped) ? d.skipped.filter(isRecord) : []
|
||||
const what = written.map((w) => {
|
||||
const key = str(w.item)
|
||||
const item = key && Object.hasOwn(CATALOG_TABLE_FIELD_LABEL, key)
|
||||
? CATALOG_TABLE_FIELD_LABEL[key as keyof typeof CATALOG_TABLE_FIELD_LABEL] : cjkText(key)
|
||||
return DC.log.catalogItem(str(w.table), str(w.source), item)
|
||||
const known = !!key && Object.hasOwn(CATALOG_TABLE_FIELD_LABEL, key)
|
||||
const item = known ? CATALOG_TABLE_FIELD_LABEL[key as keyof typeof CATALOG_TABLE_FIELD_LABEL] : cjkText(key)
|
||||
const where = DC.log.catalogItem(str(w.table), str(w.source), item)
|
||||
const value = known && w.value != null && w.value !== '' ? patchValueText(key, w.value) : ''
|
||||
return where && value ? DC.log.catalogValue(where, value) : where
|
||||
}).filter(Boolean)
|
||||
const why = skipped.map((s) => str(s.reason)).filter((r): r is string => !!r)
|
||||
const rest = skipped.length ? `${DC.log.catalogSkipped(skipped.length)}${why.length ? `:${why.join(';')}` : ''}` : ''
|
||||
@@ -1062,7 +1079,22 @@ function explainLog(code: string | undefined, message: string, d: Record<string,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
// ---- 反方审稿:同上,文案在 DUAL_CHECK_TEXT.challengeLog(时间线的解读统一在这里做)
|
||||
case 'dual_check_unchecked': {
|
||||
// metrics 是 [{id, name, reason}]:副标题点名、说原因(没起名字的写 id)
|
||||
const list = Array.isArray(d.metrics) ? d.metrics.filter(isRecord) : []
|
||||
const parts = list.map((m) => {
|
||||
const name = str(m.name) ?? str(m.id)
|
||||
const why = cjkText(m.reason)
|
||||
return name ? `「${name}」${why ? `:${why}` : ''}` : ''
|
||||
}).filter(Boolean)
|
||||
return {
|
||||
title: DC.log.unchecked(list.length || undefined),
|
||||
sub: parts.length ? parts.join(';') : undefined,
|
||||
next: DC.log.uncheckedNext,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
// ---- 反方审稿:同上,文案在 CHALLENGE_TEXT.log(时间线的解读统一在这里做)
|
||||
case 'challenge_skipped': {
|
||||
const reason = str(d.reason)
|
||||
if (reason === 'disabled') {
|
||||
@@ -1107,6 +1139,25 @@ function explainLog(code: string | undefined, message: string, d: Record<string,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
case 'challenge_unreviewed': {
|
||||
// claims_skipped:没能反查的句数;reason:第一句没能反查的原因(一句中文)
|
||||
return {
|
||||
title: CL.unreviewed(countOf(d.claims_skipped) || undefined),
|
||||
sub: cjkText(d.reason),
|
||||
next: CL.unreviewedNext,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
case 'challenge_sources_lost': {
|
||||
// claims:只查了部分数据源的句数;sources:没查成的数据源名
|
||||
const names = strs(d.sources)
|
||||
return {
|
||||
title: CL.sourcesLost(countOf(d.claims) || undefined),
|
||||
sub: names.length ? CL.sourcesLostSub(names) : undefined,
|
||||
next: CL.sourcesLostNext,
|
||||
level: 'warn',
|
||||
}
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -2051,8 +2102,10 @@ export function decodeRun(events: RunEvent[], final?: RunFinal, opts?: DecodeOpt
|
||||
if (d.missing_expected?.length) gaps.push(`缺少期望指标:${d.missing_expected.join('、')}`)
|
||||
if (num(d.unmatched)) gaps.push(`${d.unmatched} 个数字无法追溯到指标集`)
|
||||
// 校验本身没跑全(叙述渲染为空、指标集为空)也会降档。不写出来的话,
|
||||
// 时间线上就是一个光秃秃的「降档出具」,看起来和"全部通过"只差一个字
|
||||
const notRun: string[] = Array.isArray(d.gaps) ? d.gaps.map(String) : []
|
||||
// 时间线上就是一个光秃秃的「降档出具」,看起来和"全部通过"只差一个字。
|
||||
// 双盲复算、反方审稿报上来的缺口是核查查出来的问题,另起一组,不归在「校验不完整」下
|
||||
const { verify, other: notRun } = splitIssuanceGaps(Array.isArray(d.gaps) ? d.gaps.map(String) : [])
|
||||
if (verify.length) gaps.push(`${DC.verifyGaps.head}:${verify.join(';')}`)
|
||||
if (notRun.length) gaps.push(`校验不完整:${notRun.join(';')}`)
|
||||
const checked = [
|
||||
num(d.metrics_checked) != null ? `核对 ${d.metrics_checked} 个指标` : '',
|
||||
|
||||
@@ -2181,5 +2181,88 @@ await section('双盲复算与反方审稿的日志(期 3):按 code 和结
|
||||
check('……缺字段的工件 id 不当成可下钻的工件', !rows.some((s) => s?.artifact != null))
|
||||
})
|
||||
|
||||
await section('核查类日志与出具缺口(期 4):无法复算、没能反查、只查了部分数据源说人话,核查报上来的缺口另起一组', async () => {
|
||||
// 期 3 时这三个代码界面认不得,只能照原话显示;出具缺口里双盲复算、反方审稿报上来的几条读成
|
||||
// 「校验不完整:反方审稿「反方审稿」:反方审稿推翻了 1 句结论…」,前缀和正文重复,归在「校验不完整」下也不准——
|
||||
// 它们是核查查出来的问题,不是校验没做完
|
||||
const E = (seq, type, node_id, data = {}) => ({ seq, type, node_id, ts: 1790000000 + seq, data })
|
||||
const events = [
|
||||
E(1, 'run.started', null, { total: 3 }),
|
||||
E(2, 'node.started', 'dc', { node_type: 'dual_check', label: '复核销售额' }),
|
||||
// metrics 是 [{id, name, reason}]:没起名字的写 id,原因是一句中文
|
||||
E(3, 'log', 'dc', { level: 'warn', code: 'dual_check_unchecked', message: '双盲复算未能核对 2 个指标,按无法复算计入缺口',
|
||||
metrics: [{ id: 'gmv', name: '销售额', reason: '输入来自合并查询,无法单独复算' }, { id: 'aov', name: '', reason: '盲算已达查询次数上限' }] }),
|
||||
E(4, 'node.finished', 'dc', { duration_ms: 20 }),
|
||||
E(5, 'node.started', 'ch', { node_type: 'challenge', label: '审月报' }),
|
||||
E(6, 'log', 'ch', { level: 'warn', code: 'challenge_unreviewed', claims_skipped: 2, reason: '数据源「北仓库」已不存在或已停用',
|
||||
message: '反方审稿有 2 句结论未能反查,已记为无法判断:数据源「北仓库」已不存在或已停用' }),
|
||||
E(7, 'log', 'ch', { level: 'warn', code: 'challenge_sources_lost', claims: 1, sources: ['北仓库', '南仓库'],
|
||||
message: '反方审稿有 1 句结论只查了部分数据源' }),
|
||||
E(8, 'node.finished', 'ch', { duration_ms: 30 }),
|
||||
E(9, 'node.started', 'done', { node_type: 'output', label: '成果' }),
|
||||
E(10, 'issuance', 'done', { tier: 'degraded', missing_required: [], missing_expected: [], unmatched: 0, metrics_checked: 2, matched_numbers: 3,
|
||||
gaps: ['双盲复算「复核销售额」:指标「销售额」两路结果不一致,审核人选择了盲算结果', '反方审稿:推翻了 1 句结论:「东区增长主要来自新客」',
|
||||
'叙述模板渲染为空(路径可能有误)', '数据整形「双盲复算备份」:两路结果不一致'] }),
|
||||
E(11, 'node.finished', 'done', { duration_ms: 5 }),
|
||||
E(12, 'run.finished', null, { status: 'succeeded' }),
|
||||
]
|
||||
const steps = mod.decodeRun(events)
|
||||
const all = flatten(steps)
|
||||
const by = (code) => all.find((s) => s.code === code)
|
||||
const NEW = ['dual_check_unchecked', 'challenge_unreviewed', 'challenge_sources_lost']
|
||||
check('三个日志代码各有一行、挂在自己的节点下', NEW.every((c) => all.some((s) => s.kind === 'node' && s.children?.some((x) => x.code === c))),
|
||||
NEW.filter((c) => !by(c)).join('、'))
|
||||
check('……标题里没有内部代码,原话留在展开区', NEW.every((c) => !/dual_check_|challenge_/.test(`${by(c)?.title}${by(c)?.sub ?? ''}${by(c)?.next ?? ''}`))
|
||||
&& by('challenge_unreviewed')?.detail === events[5].data.message)
|
||||
const un = by('dual_check_unchecked')
|
||||
check('dual_check_unchecked:几个指标无法复算、计入缺口,副标题点名带原因(没名字的写 id),提醒色',
|
||||
un?.title === '双盲复算有 2 个指标无法复算,按缺口计入出具' && un.sub === '「销售额」:输入来自合并查询,无法单独复算;「aov」:盲算已达查询次数上限'
|
||||
&& un.level === 'warn' && !!un.next?.includes('核查'), `${un?.title} / ${un?.sub}`)
|
||||
const ur = by('challenge_unreviewed')
|
||||
check('challenge_unreviewed:几句没能反查、记为无法判断,原因放副标题', ur?.title === '反方审稿有 2 句结论没能反查,已记为无法判断'
|
||||
&& ur.sub === '数据源「北仓库」已不存在或已停用' && ur.level === 'warn' && !!ur.next?.includes('没有真正查询数据'), `${ur?.title} / ${ur?.sub}`)
|
||||
const lost = by('challenge_sources_lost')
|
||||
check('challenge_sources_lost:几句只查了部分数据源,点名没查成的源、判断不含它们', lost?.title === '反方审稿有 1 句结论只查了部分数据源'
|
||||
&& lost.sub === '未能查询:「北仓库」、「南仓库」(已不存在或已停用),判断不含其中的数据' && lost.level === 'warn' && !!lost.next?.includes('数据源'),
|
||||
`${lost?.title} / ${lost?.sub}`)
|
||||
|
||||
const issuance = steps.find((s) => s.kind === 'issuance')
|
||||
const title = issuance?.title ?? ''
|
||||
const detail = issuance?.detail ?? ''
|
||||
check('出具那一行:核查报上来的两条归在「核查未通过」下', detail.split('\n').includes(
|
||||
'核查未通过:双盲复算「复核销售额」:指标「销售额」两路结果不一致,审核人选择了盲算结果;反方审稿:推翻了 1 句结论:「东区增长主要来自新客」'), detail)
|
||||
check('……校验没做完的那条仍在「校验不完整」下,核查的不在那里', detail.split('\n').some((l) => l.startsWith('校验不完整:')
|
||||
&& l.includes('叙述模板渲染为空') && !l.includes('两路结果不一致,审核人') && !l.includes('推翻了')), detail)
|
||||
check('……只是名字里带「双盲复算」的别的节点不算核查(按节点类型的前缀认)', detail.split('\n').some((l) => l.startsWith('校验不完整:')
|
||||
&& l.includes('数据整形「双盲复算备份」:两路结果不一致')), detail)
|
||||
const lines = detail.split('\n')
|
||||
check('……核查那一组排在校验不完整前面,标题以档位开头', title.startsWith('降档出具:核查未通过:')
|
||||
&& lines.findIndex((l) => l.startsWith('核查未通过:')) < lines.findIndex((l) => l.startsWith('校验不完整:')), title)
|
||||
check('……前缀和正文不重复节点类型', !/反方审稿:反方审稿|双盲复算「[^」]*」:[^;]*双盲复算/.test(detail), detail)
|
||||
|
||||
// splitIssuanceGaps:成果卡片、证据页按同一个函数分组
|
||||
const split = mod.splitIssuanceGaps(['双盲复算:指标「订单数」无法复算:输入来自合并查询', '反方审稿「审周报」:推翻了 2 句结论:「甲」「乙」',
|
||||
'缺少必需指标:gmv', '反方审稿报告没写完'])
|
||||
check('splitIssuanceGaps:「类型:」「类型「名」:」开头的才算核查缺口,同名开头的普通文字不算', JSON.stringify(split) === JSON.stringify({
|
||||
verify: ['双盲复算:指标「订单数」无法复算:输入来自合并查询', '反方审稿「审周报」:推翻了 2 句结论:「甲」「乙」'],
|
||||
other: ['缺少必需指标:gmv', '反方审稿报告没写完'] }), JSON.stringify(split))
|
||||
|
||||
// 老日志缺字段、字段类型不对:不崩,不出 undefined、NaN、[object Object],不瞎填数
|
||||
const odd = [['dual_check_unchecked', { metrics: 'x' }], ['challenge_unreviewed', { claims_skipped: 'x', reason: 3 }],
|
||||
['challenge_sources_lost', { claims: null, sources: 'x' }]]
|
||||
let broken = null
|
||||
let rows = []
|
||||
try {
|
||||
const got = flatten(mod.decodeRun([E(1, 'node.started', 'x', { node_type: 'challenge' }),
|
||||
...odd.map(([code, data], i) => E(i + 2, 'log', 'x', { level: 'warn', code, ...data })), E(9, 'node.finished', 'x', {})]))
|
||||
rows = odd.map(([code]) => got.find((s) => s.code === code))
|
||||
} catch (e) {
|
||||
broken = String(e?.message ?? e)
|
||||
}
|
||||
check('缺字段的老日志:不抛异常,每个代码照样一行', broken === null && rows.every((r) => r?.title), broken ?? '')
|
||||
check('……不出 undefined、NaN、[object Object],也不瞎填数', !rows.some((r) => /undefined|NaN|object Object|\d/.test(`${r?.title}${r?.sub ?? ''}`)),
|
||||
rows.map((r) => `${r?.title}|${r?.sub ?? ''}`).join(' ; '))
|
||||
})
|
||||
|
||||
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 解码器全部通过')
|
||||
process.exit(failed ? 1 : 0)
|
||||
|
||||
Reference in New Issue
Block a user