出具缺口里核查报上来的另起一组「核查未通过」,时间线认得期 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:
ThearchyHelios
2026-10-06 07:53:18 +08:00
parent 9eeb11d29e
commit e35d19d014
6 changed files with 231 additions and 40 deletions
+4 -2
View File
@@ -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
View File
@@ -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: '请在「数据」页确认这些数据源是否应启用,启用后重新运行',
},
}
/**
+28 -6
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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} 个指标` : '',
+83
View File
@@ -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)