diff --git a/web_dashboard/backend/tests/test_frontend_contract_view_audit.py b/web_dashboard/backend/tests/test_frontend_contract_view_audit.py new file mode 100644 index 00000000..3bd3daa7 --- /dev/null +++ b/web_dashboard/backend/tests/test_frontend_contract_view_audit.py @@ -0,0 +1,34 @@ +from pathlib import Path +import re + + +FRONTEND_SRC = Path(__file__).resolve().parents[2] / "frontend" / "src" +CONTRACT_VIEW = FRONTEND_SRC / "utils" / "contractView.js" +LEGACY_TOP_LEVEL_FIELDS = ("decision", "confidence", "quant_signal", "llm_signal") +DIRECT_FIELD_ACCESS = re.compile(r"(?:\?|)\.\s*(decision|confidence|quant_signal|llm_signal)\b") + + +def test_contract_view_reads_contract_result_before_compat_fields(): + source = CONTRACT_VIEW.read_text() + + assert "getResult(payload).decision ?? getCompat(payload).decision" in source + assert "getResult(payload).confidence ?? getCompat(payload).confidence" in source + assert "getResult(payload).signals?.quant?.rating ?? getCompat(payload).quant_signal" in source + assert "getResult(payload).signals?.llm?.rating ?? getCompat(payload).llm_signal" in source + + +def test_frontend_consumers_use_contract_view_helpers_for_signal_fields(): + offenders: list[str] = [] + + for path in sorted(FRONTEND_SRC.rglob("*.js")) + sorted(FRONTEND_SRC.rglob("*.jsx")): + if path == CONTRACT_VIEW: + continue + matches = { + match.group(1) + for match in DIRECT_FIELD_ACCESS.finditer(path.read_text()) + if match.group(1) in LEGACY_TOP_LEVEL_FIELDS + } + if matches: + offenders.append(f"{path.relative_to(FRONTEND_SRC)} -> {sorted(matches)}") + + assert offenders == [] diff --git a/web_dashboard/frontend/src/components/ContractCues.jsx b/web_dashboard/frontend/src/components/ContractCues.jsx new file mode 100644 index 00000000..bfa973e1 --- /dev/null +++ b/web_dashboard/frontend/src/components/ContractCues.jsx @@ -0,0 +1,51 @@ +import { + getDataQualitySummary, + getDegradationSummary, + isDegradedPayload, +} from '../utils/contractView' + +const cueStyle = { + display: 'inline-flex', + alignItems: 'center', + padding: '2px 8px', + borderRadius: 'var(--radius-pill)', + background: 'var(--hold-dim)', + color: 'var(--hold)', + fontSize: 11, + fontWeight: 600, + lineHeight: 1.4, +} + +function formatCode(code) { + return String(code).replace(/_/g, ' ') +} + +export default function ContractCues({ payload, style = null }) { + const dataQuality = getDataQualitySummary(payload) + const degradation = getDegradationSummary(payload) + const primaryReason = degradation?.reason_codes?.[0] || null + const dataQualityState = dataQuality?.state || null + const items = [] + + if (isDegradedPayload(payload)) { + items.push(primaryReason && primaryReason !== dataQualityState + ? `降级 · ${formatCode(primaryReason)}` + : '降级结果') + } + + if (dataQualityState) { + items.push(`数据 · ${formatCode(dataQualityState)}`) + } + + if (items.length === 0) return null + + return ( +