降档时报告页顶部写明出具档位和原因(B5):出具声明和 issuance 事件另带结构化的 sql_checks(哪次查询、问题、受影响的引用、对应的缺口),证据页签在报告上方放出具横幅,SQL 检查导致的降档单列一块写在显眼处、不在其余缺口里重复;不是完整出具时「N 个数字都有出处」不再画成绿色

This commit is contained in:
ThearchyHelios
2026-10-04 02:33:15 +08:00
parent 31531925a9
commit 6263725c2a
12 changed files with 196 additions and 18 deletions
+17 -4
View File
@@ -466,6 +466,12 @@ def incomplete_gaps(metrics: list[dict[str, Any]]) -> list[str]:
def sql_check_gaps(metrics: list[dict[str, Any]], cited: dict[str, list[str]] | None = None, *,
label: Callable[[str, str | None], str | None] | None = None) -> list[str]:
"""sql_check_findings 的那几句缺口(见下)。"""
return [f["text"] for f in sql_check_findings(metrics, cited, label=label)]
def sql_check_findings(metrics: list[dict[str, Any]], cited: dict[str, list[str]] | None = None, *,
label: Callable[[str, str | None], str | None] | None = None) -> list[dict[str, Any]]:
"""没通过 SQL 检查(error 级:一对多关联后重复计算、存量跨期加总)的查询,每条一句缺口。
两处来历:契约收来的指标(口径卡标了 sql_check_failed,sql_check_sources 记着取数的查询快照),以及报告直接
@@ -475,9 +481,12 @@ def sql_check_gaps(metrics: list[dict[str, Any]], cited: dict[str, list[str]] |
按出问题的那条查询归并(合并查询顺着输入找到源查询):同一条查询算出三个指标、报告又引用了它的格,也只记一句,
句子里列出受影响的引用。label(快照 id, 节点 id) 给出「查询「取数」(Q1)」这样的称呼,给不出就写「所依据的查询」。
指标标了 sql_check_failed、快照却读不出来(被清理、被改过)的,退回口径卡记下的那句:缺口不能因为读不到快照就消失。
每条一个 dict:text(缺口那句)、query(称呼)、artifact、node_id、problems、refs。出具声明里另存一份
sql_checks:报告页据此在显眼处说「因为 SQL 检查没通过而降档」,不用从缺口的中文里认。
"""
groups: dict[str, dict[str, Any]] = {}
fallback: list[str] = []
fallback: list[dict[str, Any]] = []
def add(artifact: Any, ref: str) -> bool:
found = query_problems(artifact) if isinstance(artifact, str) and artifact else []
@@ -495,7 +504,8 @@ def sql_check_gaps(metrics: list[dict[str, Any]], cited: dict[str, list[str]] |
name = f"指标「{m.get('name') or m.get('id')}」"
hits = [add(src.get("artifact"), name) for src in m.get("sql_check_sources") or [] if isinstance(src, dict)]
if not any(hits):
fallback.append(f"{name}{m.get('sql_check_reason') or '所依据的查询未通过 SQL 检查'}")
fallback.append({"text": f"{name}{m.get('sql_check_reason') or '所依据的查询未通过 SQL 检查'}",
"query": None, "artifact": None, "node_id": None, "problems": [], "refs": [name]})
for artifact, refs in (cited or {}).items():
for ref in refs:
add(artifact, ref)
@@ -503,10 +513,13 @@ def sql_check_gaps(metrics: list[dict[str, Any]], cited: dict[str, list[str]] |
out = []
for artifact, group in groups.items():
problem = group["problem"]
who = (label(artifact, problem.node_id) if label else None) or "所依据的查询"
named = label(artifact, problem.node_id) if label else None
who = named or "所依据的查询"
refs = group["refs"]
listed = "、".join(refs[:_SQL_REFS]) + (f"等 {len(refs)} 处" if len(refs) > _SQL_REFS else "")
out.append(f"{who}{problem_clause(problem.problems)};受影响的引用:{listed}")
out.append({"text": f"{who}{problem_clause(problem.problems)};受影响的引用:{listed}", "query": named,
"artifact": artifact, "node_id": problem.node_id, "problems": problem.problems,
"refs": refs[:_SQL_REFS * 2]})
return out + fallback
+19 -4
View File
@@ -147,7 +147,7 @@ def _apply_contract(
) -> dict[str, Any]:
from datetime import datetime, timezone
from app.engine.issuance import decide_tier, incomplete_gaps, sql_check_gaps, trace_numbers
from app.engine.issuance import decide_tier, incomplete_gaps, sql_check_findings, trace_numbers
nodes = state.get("nodes") or {}
# 写了 report_from 就是引用模式:按报告文档逐段复核,不再按数值回指叙述
@@ -236,7 +236,17 @@ def _apply_contract(
return f"查询「{title}」({alias})"
return f"查询「{title}」" if title else (f"查询 {alias}" if alias else "")
gaps[sql_at:sql_at] = sql_check_gaps(metrics, cited.get("queries"), label=query_label)
findings = sql_check_findings(metrics, cited.get("queries"), label=query_label)
gaps[sql_at:sql_at] = [f["text"] for f in findings]
# 出具声明另存一份结构化的:报告页在显眼处说「因为 SQL 检查没通过而降档」,不从缺口的中文里认。没有就不加这个键
from app.engine.sql_problems import node_of
for f in findings:
if not f.get("node_id") and f.get("artifact"):
f["node_id"] = node_of(f["artifact"], state.get("evidence"))
# gap 是 gaps 里对应的那一句:界面单列这几条时,据它不在「其余缺口」里重复
sql_checks = [{**{k: f[k] for k in ("query", "node_id", "problems", "refs") if f.get(k)}, "gap": f["text"]}
for f in findings]
# 声明了 metrics_from 却一个指标都没收到,同样是"没查成"
if sources and not metrics:
@@ -265,7 +275,7 @@ def _apply_contract(
return _declare_citations(tier, cited, calibers=calibers, metrics=metrics, gaps=gaps,
missing_required=missing_required, missing_expected=missing_expected,
report_from=str(report_from), declared_at=datetime.now(timezone.utc),
ctx=ctx)
ctx=ctx, sql_checks=sql_checks)
# 逐个数字的出处。以前只有一个计数,界面标不出「这个数来自哪个指标」
matched = [
@@ -284,6 +294,7 @@ def _apply_contract(
# 校验没跑全的原因照实印出来——读的人要能分辨"查过都对"和"根本没查"
"gaps": gaps,
"declared_at": datetime.now(timezone.utc).isoformat(),
**({"sql_checks": sql_checks} if sql_checks else {}),
}
ctx.emit(
EventType.ISSUANCE,
@@ -298,6 +309,7 @@ def _apply_contract(
metrics_checked=len(metrics),
matched_numbers=len(trace.matched) if trace else 0,
matched=matched,
**({"sql_checks": sql_checks} if sql_checks else {}),
)
return issuance
@@ -584,7 +596,7 @@ def _cited_queries(doc: dict[str, Any], catalog: dict[str, Any]) -> tuple[dict[s
def _declare_citations(
tier: str, cited: dict[str, Any], *, calibers: list[dict[str, str]], metrics: list[dict[str, Any]],
gaps: list[str], missing_required: list[str], missing_expected: list[str], report_from: str,
declared_at: Any, ctx: NodeContext,
declared_at: Any, ctx: NodeContext, sql_checks: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""引用模式的 _issuance。旧字段一个不少(老前端照常显示),另加 mode、report、unresolved。"""
report = {"node_id": report_from, "doc_artifact": cited["doc_artifact"]}
@@ -606,6 +618,8 @@ def _declare_citations(
# 结论句策略和可疑实体:只在用上时才有这两个键,升级前的运行和没用上的契约形状不变
**({"claims": cited["claims"]} if cited.get("claims") else {}),
**({"entities": cited["entities"]} if cited.get("entities") else {}),
# 没通过 SQL 检查的查询(结构化):报告页据此在显眼处说明降档原因。没有就不加这个键
**({"sql_checks": sql_checks} if sql_checks else {}),
}
entities = cited.get("entities")
ctx.emit(
@@ -619,6 +633,7 @@ def _declare_citations(
unresolved=len(cited["unresolved"]),
calibers=calibers,
gaps=gaps,
**({"sql_checks": sql_checks} if sql_checks else {}),
metrics_checked=len(metrics),
matched_numbers=len(cited["matched"]),
matched=cited["matched"][:50],
@@ -97,6 +97,12 @@ async def test_one_failing_query_is_one_gap_and_one_timeline_row(scenic_source,
[gap] = [g for g in issuance["gaps"] if "SQL 检查" in g]
assert gap.count(PROBLEM) == 1
assert "指标「订单金额」" in gap and "指标「订单金额(整形后)」" in gap and "Q1 第 1 行「gmv」" in gap
# 出具声明和事件另带结构化的 sql_checks:报告页在显眼处说明降档原因,不从缺口的中文里认(B5)
[found] = issuance["sql_checks"]
assert found == {"query": "查询「fetch」(Q1)", "node_id": "fetch", "problems": [PROBLEM],
"refs": ["指标「订单金额」", "指标「订单金额(整形后)」", "Q1 第 1 行「gmv」"], "gap": gap}, found
[declared] = [e.data for e in await events(row.id, "issuance")]
assert declared["sql_checks"] == issuance["sql_checks"]
# 运行时间线:同一条查询的问题一行,列出受影响的指标;带着结构化字段,界面不用解析中文
[log] = [e.data for e in await events(row.id, "log", "card") if e.data.get("code") == "metric_sql_check"]
+6 -1
View File
@@ -14,7 +14,7 @@ import mdSamples from '../run/__tests__/markdown-samples.json'
import evidenceFixture from '../run/__tests__/evidence-doc.json'
import evidenceJudge from '../run/__tests__/evidence-judge.json'
import {
CALIBER_ONE_OUTPUT, CALIBERS_OUTPUT, COPILOT_CATALOG_PATCH, COPILOT_CONTEXT, COPILOT_CONTEXT_FALLBACK, COPILOT_STUCK, COPILOT_TOOLS_DROPPED, MIXED_OUTPUT, abandonedRun, cancelledRun, exhaustedTeam, longLoop,
CALIBER_ONE_OUTPUT, CALIBERS_OUTPUT, SQL_DEGRADED_OUTPUT, COPILOT_CATALOG_PATCH, COPILOT_CONTEXT, COPILOT_CONTEXT_FALLBACK, COPILOT_STUCK, COPILOT_TOOLS_DROPPED, MIXED_OUTPUT, abandonedRun, cancelledRun, exhaustedTeam, longLoop,
markupRun, mergeRun, mixedRun, parallelQueryRun, pipelineRun, repairRun, teamRun, timeoutRun,
} from '../run/__tests__/synthetic'
import '../index.css'
@@ -302,6 +302,11 @@ function synthetic(name: string): StreamTurn[] {
steps: decodeRun(ev, { status: 'succeeded' }), output: MIXED_OUTPUT, runId: 'syn-issued',
runClass: 'exploratory', review: REVIEW_DEGRADED_TURN.review }]
}
case 'issued-sql':
// 数字都有出处,却因为查询没通过 SQL 检查而降档:横幅单列说明
return [{ id: name, question: '上周订单金额', phase: 'done', status: '已完成',
steps: decodeRun(parallelQueryRun(), { status: 'succeeded' }), runId: 'syn-issued-sql', runClass: 'formal',
output: SQL_DEGRADED_OUTPUT }]
case 'calibers':
case 'caliber-one': {
// 按口径卡清点回指的数字:两张卡各数各的;只有一张卡时直说都来自它
+7
View File
@@ -2598,6 +2598,13 @@ export const RUN_SQL_CHECK_TEXT = {
/** 「打开设置」落到来源查询节点的 SQL 上 */
openSql: (label?: string) => (label ? `打开「${label}」的 SQL` : '打开来源查询的 SQL'),
metricNext: '指标照常计算,但结果不可靠,出具按缺口降档。请修改来源查询的 SQL 后重新运行;检查依据有误时,请在数据目录中修正对应项',
/** 出具横幅、报告页:SQL 检查导致的降档单列一块 */
issuanceTitle: (tier: string) => (tier === 'degraded' ? '降档原因:所依据的查询未通过 SQL 检查' : '所依据的查询未通过 SQL 检查'),
issuanceQuery: '所依据的查询',
issuanceAffected: (refs: string) => `受影响:${refs}`,
issuanceNext: '数字的出处都对得上,但算出这些数的查询本身有问题,结果不可靠。请修改来源查询的 SQL 后重新运行;检查依据有误时,请在数据目录中修正对应项',
/** 其余缺口的标题(SQL 检查的那几条单列之后) */
otherGaps: '校验未全部完成:',
}
export const COPILOT_CONTEXT_TEXT = {
+9
View File
@@ -13,6 +13,7 @@ import {
} from '../../lib/evidence'
import { formatNumber } from '../../lib/format'
import { EVIDENCE_AUDIT_TEXT as T, UPGRADE_TEXT } from '../../lib/terms'
import { IssuanceBanner } from '../../run/AssistantStream'
import { EvidenceDoc, type EvidenceDocHandle } from '../../run/EvidenceDoc'
import { EvidenceGuessView } from '../../run/EvidenceGuess'
import { Markdown, inlineStyles } from '../../run/Markdown'
@@ -142,6 +143,13 @@ export function EvidencePane({ run, output, labelOf, refreshKey }: {
<SummaryBar mode={mode} seal={seal} message={audit.data?.seal?.message}
reports={graph.reports ?? []} labelOf={labelOf} />
<UpgradeBanner run={run} mode={mode} />
{/* 出具档位和降档原因写在报告上方:降档时只在侧栏小标签里写「SQL 检查未通过」,报告上却是绿色的
「10 个数字都有出处」,读的人会以为一切正常。SQL 检查导致的降档在横幅里单列一块 */}
{!!output?._issuance?.tier && (
<div data-evidence-issuance="">
<IssuanceBanner issuance={output._issuance} runClass={run.run_class} />
</div>
)}
{mode === 'cited' && reports.map((r) => {
const doc = docOf(r.node_id)
@@ -157,6 +165,7 @@ export function EvidencePane({ run, output, labelOf, refreshKey }: {
{doc ? (
<EvidenceDoc ref={(h) => { handles.current.set(r.node_id!, h) }} doc={doc} artifact={r.doc_artifact}
runId={run.id} runClass={run.run_class} label={r.fields?.join('、') || r.node_id} tally
tier={typeof output?._issuance?.tier === 'string' ? output._issuance.tier : undefined}
panel={wide ? 'dock' : 'auto'} dock={dock} onView={(o) => onView(r.node_id!, o)} />
) : slot?.status === 'error' ? (
<p className="text-2xs" style={{ color: 'var(--st-failed)' }}>{T.docGone(labelOf(r.node_id) ?? r.node_id ?? '')}</p>
+39 -5
View File
@@ -25,7 +25,7 @@ import {
import { CODE_COLUMN, CopyChip, LEADING_ZERO, Markdown, type MarkSpec } from './Markdown'
import { ClaimTally, EvidenceField, type EvidenceDocHandle } from './EvidenceDoc'
import { EVIDENCE_STATE, claimProblems, docTally, evidenceFields, issuanceMarks, type EvidenceTally } from '../lib/evidence'
import type { EvidenceDocData } from '../types'
import type { EvidenceDocData, IssuanceSqlCheck } from '../types'
import { useRunClock } from './useRunClock'
import { CatalogPatchCard } from './CatalogPatchCard'
import type { CatalogPatch } from './catalogPatch'
@@ -2262,6 +2262,32 @@ function OutputValue({ value, marks, label }: { value: unknown; marks?: MarkSpec
)
}
/**
* 出具声明里没通过 SQL 检查的查询(sql_checks):每条写是哪次查询、什么问题、影响了哪些引用,下一步写改来源查询的 SQL。
* 出具横幅和报告页顶部共用。颜色跟着档位:降档用提醒色,不予出具用失败色
*/
export function IssuanceSqlChecks({ tier, checks }: { tier: string; checks: IssuanceSqlCheck[] }) {
const tone = tier === 'withheld' ? 'var(--st-failed)' : 'var(--st-waiting)'
return (
<div className="mt-1.5 rounded border px-2 py-1.5 text-2xs leading-relaxed" data-issuance-sql-checks={checks.length}
style={{ borderColor: `color-mix(in srgb, ${tone} 45%, var(--border))`, background: `color-mix(in srgb, ${tone} 6%, transparent)` }}>
<div className="flex items-center gap-1 font-medium" style={{ color: tone }}>
<AlertTriangle size={11} aria-hidden /> {RUN_SQL_CHECK_TEXT.issuanceTitle(tier)}
</div>
<ul className="mt-0.5 space-y-0.5">
{checks.map((c, i) => (
<li key={`${c.query ?? ''}:${i}`} className="[overflow-wrap:anywhere]" data-issuance-sql-check={c.node_id ?? ''}>
<span className="font-medium text-fg">{c.query || RUN_SQL_CHECK_TEXT.issuanceQuery}</span>
{!!c.problems?.length && <span className="text-fg">:{c.problems.join(';')}</span>}
{!!c.refs?.length && <span className="text-dim">({RUN_SQL_CHECK_TEXT.issuanceAffected(c.refs.join('、'))})</span>}
</li>
))}
</ul>
<div className="mt-0.5 text-dim">{RUN_SQL_CHECK_TEXT.issuanceNext}</div>
</div>
)
}
const TIER_META: Record<string, { color: string; soft: string; hint: string }> = {
formal: { color: 'var(--st-done)', soft: 'var(--st-done-soft)', hint: ISSUANCE_HINT.formal },
degraded: { color: 'var(--st-waiting)', soft: 'var(--st-waiting-soft)', hint: ISSUANCE_HINT.degraded },
@@ -2292,7 +2318,12 @@ export function IssuanceBanner({ issuance, runClass, evidence }: {
if (!tier) return null
const meta = TIER_META[tier] ?? { color: 'var(--st-waiting)', soft: 'var(--st-waiting-soft)', hint: '' }
const unmatched: any[] = issuance?.unmatched_numbers ?? []
const gaps: string[] = Array.isArray(issuance?.gaps) ? issuance.gaps.map(String) : []
// 没通过 SQL 检查的查询(出具声明的 sql_checks)单列一块、写在显眼处:数字都有出处,降档却是因为算数的查询本身有问题。
// 单列了的那几句(gap)不在其余缺口里重复
const sqlChecks: IssuanceSqlCheck[] = (Array.isArray(issuance?.sql_checks) ? issuance.sql_checks : [])
.filter((x: unknown): x is IssuanceSqlCheck => !!x && typeof x === 'object')
const sqlGaps = new Set(sqlChecks.map((x) => x.gap).filter(Boolean))
const gaps: string[] = (Array.isArray(issuance?.gaps) ? issuance.gaps.map(String) : []).filter((g: string) => !sqlGaps.has(g))
const calibers: any[] = issuance?.calibers ?? []
const matched = Number(issuance?.matched_numbers ?? 0)
// 逐个数字的出处(matched[].caliber 形如「口径名 @ v2」):有它就能按卡数清楚,
@@ -2309,6 +2340,8 @@ export function IssuanceBanner({ issuance, runClass, evidence }: {
const uncitedClaims = Number(issuance?.claims?.uncited_claims ?? 0) || 0
const numbers = counts ? evidenceTally(counts.cited, counts.total, counts.other) : ''
const numbersOk = !counts?.none && !counts?.other
// 数字都有出处只在完整出具时用绿色:降档时「10 个数字都有出处」画成绿的,读的人会以为一切正常
const numbersColor = !numbersOk ? 'var(--st-waiting)' : tier === 'formal' ? 'var(--st-done)' : 'var(--text-dim)'
// 四期:结论句有判定(开了裁判,或探索运行里按需判过)时多一段「结论 4 句(支持 3 · 无证据 1)」。
// 没开裁判的文档里没挂依据的都数在这一段的「无证据」里,不再另起一句;开了裁判的文档里,没挂依据却送了
// 裁判的句子按判定数、预筛放掉的不数——出具照样按没挂依据计缺口(require_citation、judge 都是),这时
@@ -2340,7 +2373,7 @@ export function IssuanceBanner({ issuance, runClass, evidence }: {
// 结论句靠句末徽标定位;只是没挂依据的结论句没有徽标,定位不到
<div className="mt-1 flex flex-wrap items-center gap-2 text-2xs" data-evidence-line="">
{numbers && (
<span className="tnum" style={{ color: numbersOk ? 'var(--st-done)' : 'var(--st-waiting)' }}>{numbers}</span>
<span className="tnum" style={{ color: numbersColor }} data-evidence-line-numbers="">{numbers}</span>
)}
{suspect > 0 && (
<span className="tnum" style={{ color: 'var(--st-waiting)' }} data-evidence-line-suspect="">
@@ -2362,9 +2395,10 @@ export function IssuanceBanner({ issuance, runClass, evidence }: {
</div>
)}
{!!sqlChecks.length && <IssuanceSqlChecks tier={tier} checks={sqlChecks} />}
{!!gaps.length && (
<div className="mt-1.5 text-2xs leading-relaxed" style={{ color: 'var(--st-waiting)' }}>
<div className="font-medium">校验未全部完成:</div>
<div className="mt-1.5 text-2xs leading-relaxed" style={{ color: 'var(--st-waiting)' }} data-issuance-gaps="">
<div className="font-medium">{RUN_SQL_CHECK_TEXT.otherGaps}</div>
<ul className="mt-0.5 space-y-0.5 pl-2">
{gaps.map((g) => <li key={g}>· {g}</li>)}
</ul>
+13 -4
View File
@@ -212,7 +212,13 @@ export const EvidenceDoc = forwardRef<EvidenceDocHandle, {
* (on_demand / inline),没开的看这里,不给就按运行 id 取一次
*/
runClass?: string
}>(function EvidenceDoc({ doc, artifact, runId, dense = false, label, panel = 'auto', dock, onView, tally = false, runClass }, ref) {
/**
* 这次出具的档位(output._issuance.tier)。不是完整出具时,「N 个数字都有出处」不画成绿色:
* 数字的出处都对得上,档位却因为别的原因(比如查询没通过 SQL 检查)降了
*/
tier?: string
}>(function EvidenceDoc({ doc, artifact, runId, dense = false, label, panel = 'auto', dock, onView, tally = false, runClass,
tier }, ref) {
const blocks = useMemo(() => doc.blocks ?? [], [doc])
const report = doc.node_id || undefined
// 封存之后按需追加的判定:盖过文档里的「未裁判 · 按需」。只换句末徽标,封存的文档一个字不改
@@ -447,7 +453,7 @@ export const EvidenceDoc = forwardRef<EvidenceDocHandle, {
<p id={summaryId} className="sr-only" data-evidence-summary="">{summary}</p>
<div className="sr-only" aria-live="polite" aria-atomic="true">{announce}</div>
{tally && (counts.total > 0 || counts.other > 0 || !!counts.suspect || !!claims?.total) && (
<EvidenceTally counts={{ ...counts, claims }} onNext={() => {
<EvidenceTally counts={{ ...counts, claims }} tier={tier} onNext={() => {
const target = step(model.alerts, current, 1)
if (target) { focusSeg(target, { flash: true }); openSeg(target) } else openViolations()
}} onList={doc.violations?.length ? openViolations : undefined} />
@@ -520,10 +526,12 @@ function usePanelMode(panel: 'auto' | PanelMode, dense: boolean): PanelMode {
* 证据条:没有出具横幅时由文档自己说「7/12 数字有出处 · 无证据 5 · 另有 1 处引用解析不了」,
* 有结论句判定时接着说「结论 4 句(支持 3 · 不支持 1)」
*/
export function EvidenceTally({ counts, onNext, onList }: {
export function EvidenceTally({ counts, onNext, onList, tier }: {
counts: Pick<Tally, 'total' | 'cited' | 'none' | 'other' | 'suspect' | 'claims'>
onNext?: () => void
onList?: () => void
/** 出具档位:给了、又不是完整出具时,数字都有出处也不用绿色 */
tier?: string
}) {
const suspect = counts.suspect ?? 0
const claims = counts.claims
@@ -532,7 +540,8 @@ export function EvidenceTally({ counts, onNext, onList }: {
return (
<div className="mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs" data-evidence-tally="">
{numbers && (
<span className="tnum" style={{ color: !counts.none && !counts.other ? 'var(--st-done)' : 'var(--st-waiting)' }}>
<span className="tnum" data-evidence-tally-numbers=""
style={{ color: counts.none || counts.other ? 'var(--st-waiting)' : !tier || tier === 'formal' ? 'var(--st-done)' : 'var(--text-dim)' }}>
{numbers}
</span>
)}
+23
View File
@@ -188,6 +188,29 @@ export const CALIBERS_OUTPUT = {
},
}
/**
* 数字都有出处,却因为算数的查询没通过 SQL 检查而降档(engine/nodes/io.py 的 sql_checks):横幅单列一块说明,
* 那一句不在其余缺口里重复;「都有出处」不画成绿色
*/
export const SQL_GAP = `查询「订单金额查询」(Q1)未通过 SQL 检查(${'「订单」关联「订单明细」是一对多,对「订单」的「订单金额」求和会重复计算'}),结果不可靠;受影响的引用:指标「订单金额」、Q1 第 1 行「gmv」`
export const SQL_DEGRADED_OUTPUT = {
answer: '上周订单金额 **119,160** 元。',
_issuance: {
tier: 'degraded',
calibers: [{ node: 'k1', caliber: '订单口径', version: 'v2' }],
metrics_checked: 1,
missing_required: [],
missing_expected: [],
unmatched_numbers: [],
matched_numbers: 1,
matched: [{ token: '119,160', metric: 'gmv', caliber: '订单口径 @ v2' }],
gaps: [SQL_GAP, '协作团队「分析团队」用完 2 轮仍未完成,交付的是成员最后的回复'],
sql_checks: [{ query: '查询「订单金额查询」(Q1)', node_id: 'q_a',
problems: ['「订单」关联「订单明细」是一对多,对「订单」的「订单金额」求和会重复计算'],
refs: ['指标「订单金额」', 'Q1 第 1 行「gmv」'], gap: SQL_GAP }],
},
}
/** 只有一张口径卡、逐个出处都指向它:直说「都来自这张卡」 */
export const CALIBER_ONE_OUTPUT = {
answer: '上周订单 **128** 单,已付款 **96** 单。',
+15
View File
@@ -2112,6 +2112,21 @@ export interface EvidenceMergeInput {
checks?: SqlCheckItem[]
}
/**
* 出具声明(output._issuance、issuance 事件)里的 sql_checks:没通过 SQL 检查的查询,一条一个。报告页、出具横幅据此
* 在显眼处说「因为 SQL 检查没通过而降档」。gap 是 gaps 里对应的那一句(单列之后不在其余缺口里重复)
*/
export interface IssuanceSqlCheck {
/** 「查询「取数」(Q1)」;给不出时没有 */
query?: string
node_id?: string
/** 每条问题那半句 */
problems?: string[]
/** 受影响的引用:「指标「订单金额」」「Q1 第 1 行「gmv」」 */
refs?: string[]
gap?: string
}
/** 查询用到的一张表、查询当时它的数据目录版本 */
export interface EvidenceCatalogVersion {
/** 表结构里的表名 */
+31
View File
@@ -1981,6 +1981,37 @@ await section('证据页签:左边报告、右边常驻面板、下方审计
await page.keyboard.press('Escape')
})
await section('证据页签:降档时报告上方写明档位和原因,SQL 检查没通过的单独说,「都有出处」不画成绿色(B5)', async () => {
// 成果的 _issuance 带 sql_checks(engine/nodes/io.py):数字都有出处,降档却是因为算数的查询没通过 SQL 检查
const EV_SQL = 'fake0evsqldegrade000000000000'
const PROBLEM = '「订单」关联「订单明细」是一对多,对「订单」的「订单金额」求和会重复计算'
const GAP = `查询「取订单」(Q1)未通过 SQL 检查(${PROBLEM}),结果不可靠;受影响的引用:Q1 第 1 行「gmv」`
const issuance = { tier: 'degraded', calibers: [], metrics_checked: 0, missing_required: [], missing_expected: [],
unmatched_numbers: [], matched_numbers: 10, matched: [], gaps: [GAP],
sql_checks: [{ query: '查询「取订单」(Q1)', node_id: 'fetch', problems: [PROBLEM], refs: ['Q1 第 1 行「gmv」'], gap: GAP }] }
evFakes(EV_SQL, { output: { ...fxe.output, _issuance: issuance }, graph: fxe.graph, audit: fxe.audit, csv: fxe.audit_csv })
await page.route(new RegExp(`/api/runs/${EV_SQL}/evidence/segments/`), (r) => r.fulfill({ status: 404, json: { detail: '没有这个片段' } }))
try {
await openEvidence(EV_SQL)
await page.locator('[data-evidence-report=write] [data-evidence-doc]').waitFor({ timeout: 5000 }).catch(() => {})
const banner = page.locator('[data-view-pane=evidence] [data-evidence-issuance] [data-issuance-banner=degraded]')
check('报告上方写明出具档位', await banner.count() === 1 && (await banner.innerText().catch(() => '')).includes('降档出具'))
const sql = (await banner.locator('[data-issuance-sql-checks]').innerText().catch(() => '')).replace(/\s+/g, ' ')
check('……SQL 检查导致的降档在显眼处说明:哪次查询、什么问题、受影响的引用', sql.includes('降档原因:所依据的查询未通过 SQL 检查')
&& sql.includes('查询「取订单」(Q1)') && sql.includes(PROBLEM) && sql.includes('Q1 第 1 行「gmv」'), sql)
check('……那一句不在其余缺口里重复', await banner.locator('[data-issuance-gaps]').count() === 0)
const tally = page.locator('[data-evidence-report=write] [data-evidence-tally-numbers]')
const [color, done] = await Promise.all([tally.evaluate((el) => getComputedStyle(el).color).catch(() => ''),
page.evaluate(() => { const s = document.createElement('span'); s.style.color = 'var(--st-done)'; document.body.append(s)
const c = getComputedStyle(s).color; s.remove(); return c })])
check('降档时报告上的数字计数不画成绿色', !!color && color !== done, `${color} vs ${done}`)
} finally {
for (const k of [...fakes.keys()]) if (k.includes(EV_SQL)) fakes.delete(k)
}
await openEvidence(EV)
check('没有出具契约的运行:报告上方不出出具横幅', await page.locator('[data-view-pane=evidence] [data-evidence-issuance]').count() === 0)
})
await section('证据页签:同一段文字里好几条同样的违规、正文里点不开的行', async () => {
// 夹具同样是后端真跑的:粗体、链接里反引号写的可疑名字只记违规、不切片段——同一段文字上两条 unknown_entity、
// 两条 unverified_entity(片段和 code 都相同),列表序号 100. 是结构片段上的裸数字。这几行正文里都点不开
+11
View File
@@ -1303,6 +1303,17 @@ await section('放弃、结构化的报错、恢复的轮次、出具横幅(RE
check('恢复的轮次:头部照样写几次查询', head.includes('3 次查询'), head.replace(/\n/g, ' '))
await r.page.close()
// 降档是因为查询没通过 SQL 检查(B5):横幅单列一块写是哪次查询、什么问题、影响了哪些引用;那一句不在其余缺口里重复
const q = await open('syn=issued-sql', { w: 1100, h: 900, name: 'issued-sql' })
const banner = q.page.locator('[data-issuance-banner=degraded]')
const sql = (await banner.locator('[data-issuance-sql-checks]').innerText().catch(() => '')).replace(/\s+/g, ' ')
check('SQL 检查导致的降档单列一块:写降档原因、哪次查询、问题、受影响的引用、下一步', sql.includes('降档原因:所依据的查询未通过 SQL 检查')
&& sql.includes('查询「订单金额查询」(Q1)') && sql.includes('求和会重复计算') && sql.includes('受影响:指标「订单金额」、Q1 第 1 行「gmv」')
&& sql.includes('修改来源查询的 SQL'), sql)
const rest = (await banner.locator('[data-issuance-gaps]').innerText().catch(() => '')).replace(/\s+/g, ' ')
check('……那一句不在「校验未全部完成」里重复,其余缺口照列', !rest.includes('未通过 SQL 检查') && rest.includes('协作团队'), rest)
await q.page.close()
const i = await open('syn=issued', { w: 1100, h: 900 })
check('出具横幅带 data-issuance-banner(画布印章据此滚过来)', await i.page.locator('[data-issuance-banner]').count() === 1)
const live = await i.page.locator('[data-turn] [role="status"][aria-live="polite"]').first().innerText()