mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
评测页分析期望的参数格和分析卡用同一份:CaseEditor 原来自抄的 PARAM_FIELDS 去掉,改用 canvas/analysisItems.paramSpecs(差异检验的比率、均值、逐行三模式也带上期次三格了,和服务器参数模型一致);参数叫法只剩 lib/termsAnalysis 一份(EVAL_TEXT.analysis.param/basis/grain 引用 ANALYSIS_TEXT.node),格子的取值转换也用 paramText/paramValue,期次格保留 {{ }} 时间变量
- 参数格:basis、grain 两份下拉带上空着的缺省项(和分析卡同两行),逐行的值列、只比这几对等叫法跟分析卡一致 - test_frontend_eval_unions.py 新增一条:paramSpecs(含 PERIODS)的参数名和服务端各参数模型的交集/并集逐个对得上、评测页引用的是 paramSpecs、三张叫法表引用 ANALYSIS_TEXT.node
This commit is contained in:
@@ -14,6 +14,10 @@ TYPES_EVAL = ROOT / "frontend" / "src" / "typesEval.ts"
|
|||||||
TERMS_EVAL = ROOT / "frontend" / "src" / "lib" / "termsEval.ts"
|
TERMS_EVAL = ROOT / "frontend" / "src" / "lib" / "termsEval.ts"
|
||||||
#: 分析方法、模式的叫法只有这一份(termsEval 引用它,不另抄)
|
#: 分析方法、模式的叫法只有这一份(termsEval 引用它,不另抄)
|
||||||
TERMS_ANALYSIS = ROOT / "frontend" / "src" / "lib" / "termsAnalysis.ts"
|
TERMS_ANALYSIS = ROOT / "frontend" / "src" / "lib" / "termsAnalysis.ts"
|
||||||
|
#: 分析卡的一项摆哪几个参数格(评测页的分析期望和分析卡用同一份)
|
||||||
|
ANALYSIS_ITEMS = ROOT / "frontend" / "src" / "canvas" / "analysisItems.ts"
|
||||||
|
#: 评测页题目编辑框(分析期望那一块)
|
||||||
|
CASE_EDITOR = ROOT / "frontend" / "src" / "pages" / "evals" / "CaseEditor.tsx"
|
||||||
|
|
||||||
_COMMENT = re.compile(r"/\*.*?\*/|//[^\n]*", re.S)
|
_COMMENT = re.compile(r"/\*.*?\*/|//[^\n]*", re.S)
|
||||||
|
|
||||||
@@ -138,3 +142,32 @@ def test_analysis_expectations_follow_the_server():
|
|||||||
terms = _source(TERMS_EVAL)
|
terms = _source(TERMS_EVAL)
|
||||||
assert re.search(r"\n\s*op: ANALYSIS_OP_LABEL\b", terms) and re.search(r"\n\s*mode: ANALYSIS_MODE_LABEL\b", terms), \
|
assert re.search(r"\n\s*op: ANALYSIS_OP_LABEL\b", terms) and re.search(r"\n\s*mode: ANALYSIS_MODE_LABEL\b", terms), \
|
||||||
"评测页的分析方法、模式叫法要引用 lib/termsAnalysis 的那一份"
|
"评测页的分析方法、模式叫法要引用 lib/termsAnalysis 的那一份"
|
||||||
|
|
||||||
|
|
||||||
|
def test_analysis_param_grids_follow_the_card_and_the_server():
|
||||||
|
"""分析期望的参数格和分析卡的一项同一份(canvas/analysisItems.paramSpecs):评测页(CaseEditor)引用它,不另抄。
|
||||||
|
参数名对得上服务端各参数模型(tools/builtin/analysis.param_names);叫法只有 lib/termsAnalysis 一份
|
||||||
|
(ANALYSIS_TEXT.node.param / basis / grain),评测页(EVAL_TEXT.analysis.param / basis / grain)引用它。"""
|
||||||
|
from app.eval.judgements import VERDICT_OPS, param_names
|
||||||
|
|
||||||
|
items = _source(ANALYSIS_ITEMS)
|
||||||
|
grid = re.search(r"export function paramSpecs\(op: AnalysisOp.*?\n\}\n", items, re.S)
|
||||||
|
assert grid is not None, "analysisItems.ts 里没有 export function paramSpecs"
|
||||||
|
# 期次三格(period、base、target)写在上面的 PERIODS 常量里,一起算
|
||||||
|
periods = re.search(r"const PERIODS: ParamSpec\[\] = \[(.*?)\]\n", items, re.S)
|
||||||
|
assert periods is not None, "analysisItems.ts 里没有 const PERIODS"
|
||||||
|
grid_keys = {a or b for a, b in re.findall(r"(?:col|cols|val)\('(\w+)'\)|key: '(\w+)'",
|
||||||
|
grid.group(0) + periods.group(1))}
|
||||||
|
# 服务端各参数模型里期望能写的那些(去掉输入、总数核对、下钻、两份查询),格子里一个不少、一个不多
|
||||||
|
server = {k for op in ("decompose", *VERDICT_OPS) for k in param_names(op)}
|
||||||
|
assert grid_keys == server, f"格子和服务端的参数模型对不上:少 {server - grid_keys},多 {grid_keys - server}"
|
||||||
|
editor = _source(CASE_EDITOR)
|
||||||
|
assert "paramSpecs(" in editor and "PARAM_FIELDS" not in editor, "评测页的参数格要引用分析卡的那一份(paramSpecs)"
|
||||||
|
terms = _source(TERMS_EVAL)
|
||||||
|
for table in ("param", "basis", "grain"):
|
||||||
|
assert re.search(rf"\n\s*{table}: ANALYSIS_TEXT\.node\.{table}\b", terms), \
|
||||||
|
f"评测页的 {table} 叫法要引用 lib/termsAnalysis 的那一份"
|
||||||
|
analysis = _source(TERMS_ANALYSIS)
|
||||||
|
labels = re.search(r"\n\s*param: \{(.*?)\} as Record<string, string>", analysis, re.S)
|
||||||
|
assert labels is not None, "termsAnalysis.ts 里没有 ANALYSIS_TEXT.node.param"
|
||||||
|
assert set(grid_keys) <= set(re.findall(r"(\w+): '", labels.group(1))), "参数格里有叫不出名字的参数"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
EvalTrigger, EvalUpgradePolicy, EvalVerdict,
|
EvalTrigger, EvalUpgradePolicy, EvalVerdict,
|
||||||
} from '../typesEval'
|
} from '../typesEval'
|
||||||
import { formatCost, formatNumber } from './format'
|
import { formatCost, formatNumber } from './format'
|
||||||
import { ANALYSIS_MODE_LABEL, ANALYSIS_OP_LABEL } from './termsAnalysis'
|
import { ANALYSIS_MODE_LABEL, ANALYSIS_OP_LABEL, ANALYSIS_TEXT } from './termsAnalysis'
|
||||||
|
|
||||||
/** 比率写成百分数:0.756 → 「75.6%」;没有分母(null)写「—」 */
|
/** 比率写成百分数:0.756 → 「75.6%」;没有分母(null)写「—」 */
|
||||||
export function pct(rate: number | null | undefined, digits = 1): string {
|
export function pct(rate: number | null | undefined, digits = 1): string {
|
||||||
@@ -336,14 +336,10 @@ export const EVAL_TEXT = {
|
|||||||
ratio: (numerator: string, denominator: string) => `${numerator} ÷ ${denominator}`,
|
ratio: (numerator: string, denominator: string) => `${numerator} ÷ ${denominator}`,
|
||||||
/** 答案里同类的分析没有做成 */
|
/** 答案里同类的分析没有做成 */
|
||||||
refused: (reason: string) => `没有做成:${reason}`,
|
refused: (reason: string) => `没有做成:${reason}`,
|
||||||
param: {
|
/** 参数格的叫法只有 lib/termsAnalysis 一份(ANALYSIS_TEXT.node.param / basis / grain):分析卡和评测页的分析期望按同一份参数名 */
|
||||||
by: '维度列', period: '期次列', base: '基期', target: '本期', measure: '度量列', numerator: '分子列', denominator: '分母列',
|
param: ANALYSIS_TEXT.node.param,
|
||||||
product: '乘积列', factors: '因子列', label: '度量的叫法', basis: '比较方式', grain: '粒度', lag: '和往前第几期比',
|
basis: ANALYSIS_TEXT.node.basis,
|
||||||
pairs: '比哪几对', reference: '对照组', mean: '均值列', sd: '标准差列', sum: '合计列', sumsq: '平方和列', n: '样本数列',
|
grain: ANALYSIS_TEXT.node.grain,
|
||||||
value: '逐行的值列',
|
|
||||||
} as Record<string, string>,
|
|
||||||
basis: { change: '和比较期比的变化率', level: '值本身' } as Record<string, string>,
|
|
||||||
grain: { day: '天', week: '周', month: '月', quarter: '季度' } as Record<string, string>,
|
|
||||||
},
|
},
|
||||||
upgrade: {
|
upgrade: {
|
||||||
title: (seq: number) => `第 ${seq} 题:处置口径升版`,
|
title: (seq: number) => `第 ${seq} 题:处置口径升版`,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { Field, Modal, Notice, Spinner, toast, useRadioGroup } from '../../compo
|
|||||||
import { errorMessage } from '../../lib/errors'
|
import { errorMessage } from '../../lib/errors'
|
||||||
import { formatDateTime, formatTime, shortId } from '../../lib/format'
|
import { formatDateTime, formatTime, shortId } from '../../lib/format'
|
||||||
import { EVAL_TEXT } from '../../lib/termsEval'
|
import { EVAL_TEXT } from '../../lib/termsEval'
|
||||||
|
import { ANALYSIS_TEXT, isAnalysisOp } from '../../lib/termsAnalysis'
|
||||||
|
import { paramSpecs, paramText, paramValue, type ParamSpec } from '../../canvas/analysisItems'
|
||||||
import { useCatalog } from '../../store/catalog'
|
import { useCatalog } from '../../store/catalog'
|
||||||
import type { Run } from '../../types'
|
import type { Run } from '../../types'
|
||||||
import type {
|
import type {
|
||||||
@@ -509,35 +511,19 @@ function SqlPart({ suiteId: _suiteId, sourceId, setSourceId, sql, setSql, previe
|
|||||||
// 分析期望:贡献、判定
|
// 分析期望:贡献、判定
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type ParamKind = 'column' | 'columns' | 'text' | 'int' | 'choice' | 'pairs'
|
|
||||||
interface ParamField { key: string; kind: ParamKind; choices?: Record<string, string> }
|
|
||||||
|
|
||||||
const A = EVAL_TEXT.analysis
|
const A = EVAL_TEXT.analysis
|
||||||
/** 判定期望能用的分析(贡献恒为变化拆解)、各自的模式(同服务端 contract.MODES) */
|
/** 判定期望能用的分析(贡献恒为变化拆解)、各自的模式(同服务端 contract.MODES) */
|
||||||
const VERDICT_OPS: EvalAnalysisOp[] = ['baseline', 'trend', 'differ', 'comparable']
|
const VERDICT_OPS: EvalAnalysisOp[] = ['baseline', 'trend', 'differ', 'comparable']
|
||||||
const OP_MODES: Partial<Record<EvalAnalysisOp, EvalAnalysisMode[]>> = {
|
const OP_MODES: Partial<Record<EvalAnalysisOp, EvalAnalysisMode[]>> = {
|
||||||
decompose: ['additive', 'ratio', 'factor'], differ: ['rate', 'mean', 'rows'],
|
decompose: ['additive', 'ratio', 'factor'], differ: ['rate', 'mean', 'rows'],
|
||||||
}
|
}
|
||||||
const col = (key: string): ParamField => ({ key, kind: 'column' })
|
/**
|
||||||
const PERIODS: ParamField[] = [col('period'), { key: 'base', kind: 'text' }, { key: 'target', kind: 'text' }]
|
* 每种分析(模式)在表单里填哪几个参数:和分析卡的一项同一份(canvas/analysisItems.paramSpecs),不另抄。
|
||||||
const LABEL: ParamField = { key: 'label', kind: 'text' }
|
* 那张表里本来就没有评测期望不许写的几样——输入(query)、总数核对(headline)、下钻(parent)、两份查询
|
||||||
const GRAIN: ParamField = { key: 'grain', kind: 'choice', choices: A.grain }
|
* (base_query、target_query):期望的输入只有期望 SQL 这一份快照(服务端 judgements._NOT_IN_EXPECTATION)
|
||||||
const GROUPS: ParamField[] = [{ key: 'by', kind: 'columns' }, { key: 'pairs', kind: 'pairs' }, { key: 'reference', kind: 'text' }]
|
*/
|
||||||
/** 每种分析(模式)在表单里填哪几个参数:参数名同分析工具(服务端按同一份参数定义校验),叫法在 termsEval */
|
const fieldsOf = (e: EvalExpectation): ParamSpec[] =>
|
||||||
const PARAM_FIELDS: Record<string, ParamField[]> = {
|
e.op && isAnalysisOp(e.op) ? paramSpecs(e.op, e.mode) : []
|
||||||
'decompose:additive': [{ key: 'by', kind: 'columns' }, ...PERIODS, col('measure'), LABEL],
|
|
||||||
'decompose:ratio': [{ key: 'by', kind: 'columns' }, ...PERIODS, col('numerator'), col('denominator'), LABEL],
|
|
||||||
'decompose:factor': [...PERIODS, col('product'), { key: 'factors', kind: 'columns' }, LABEL],
|
|
||||||
baseline: [col('period'), col('measure'), { key: 'target', kind: 'text' },
|
|
||||||
{ key: 'basis', kind: 'choice', choices: A.basis }, GRAIN, { key: 'lag', kind: 'int' }, LABEL],
|
|
||||||
trend: [col('period'), col('measure'), GRAIN, LABEL],
|
|
||||||
'differ:rate': [...GROUPS, col('numerator'), col('denominator'), LABEL],
|
|
||||||
'differ:mean': [...GROUPS, col('mean'), col('sd'), col('sum'), col('sumsq'), col('n'), LABEL],
|
|
||||||
'differ:rows': [...GROUPS, col('value'), LABEL],
|
|
||||||
comparable: [...PERIODS, col('measure'), GRAIN],
|
|
||||||
}
|
|
||||||
const fieldsOf = (e: EvalExpectation): ParamField[] =>
|
|
||||||
PARAM_FIELDS[e.op === 'decompose' || e.op === 'differ' ? `${e.op}:${e.mode}` : e.op ?? ''] ?? []
|
|
||||||
|
|
||||||
/** 换了分析方法、模式时,只留下新的那种分析还认的参数 */
|
/** 换了分析方法、模式时,只留下新的那种分析还认的参数 */
|
||||||
function keepParams(e: EvalExpectation): EvalExpectation {
|
function keepParams(e: EvalExpectation): EvalExpectation {
|
||||||
@@ -607,7 +593,7 @@ function AnalysisPick({ columns, main, setMain }: {
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
{fieldsOf(current).map((f) => (
|
{fieldsOf(current).map((f) => (
|
||||||
<ParamInput key={`${current.op}:${current.mode}:${f.key}`} field={f} columns={columns}
|
<ParamInput key={`${current.op}:${current.mode}:${f.key}`} spec={f} columns={columns}
|
||||||
value={(current.params ?? {})[f.key]} onChange={(v) => setParam(f.key, v)} />
|
value={(current.params ?? {})[f.key]} onChange={(v) => setParam(f.key, v)} />
|
||||||
))}
|
))}
|
||||||
{fieldsOf(current).some((f) => f.key === 'base') && (
|
{fieldsOf(current).some((f) => f.key === 'base') && (
|
||||||
@@ -619,15 +605,17 @@ function AnalysisPick({ columns, main, setMain }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ParamInput({ field, columns, value, onChange }: {
|
function ParamInput({ spec, columns, value, onChange }: {
|
||||||
field: ParamField; columns: string[]; value: unknown; onChange: (v: unknown) => void
|
spec: ParamSpec; columns: string[]; value: unknown; onChange: (v: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const id = `eval-analysis-param-${field.key}`
|
const key = spec.key
|
||||||
const label = A.param[field.key] ?? field.key
|
const id = `eval-analysis-param-${key}`
|
||||||
if (field.kind === 'columns') {
|
const label = A.param[key] ?? key
|
||||||
|
const text = paramText(spec.kind, value)
|
||||||
|
if (spec.kind === 'columns') {
|
||||||
const picked = Array.isArray(value) ? value.map(String) : []
|
const picked = Array.isArray(value) ? value.map(String) : []
|
||||||
return (
|
return (
|
||||||
<div className="sm:col-span-2" data-eval-analysis-param={field.key}>
|
<div className="sm:col-span-2" data-eval-analysis-param={key}>
|
||||||
<div className="label">{label}</div>
|
<div className="label">{label}</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{columns.map((c) => (
|
{columns.map((c) => (
|
||||||
@@ -641,34 +629,46 @@ function ParamInput({ field, columns, value, onChange }: {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (field.kind === 'pairs') {
|
if (spec.kind === 'pairs') {
|
||||||
const text = Array.isArray(value) ? value.map((p) => (Array.isArray(p) ? p.join(',') : String(p))).join('\n') : ''
|
|
||||||
return (
|
return (
|
||||||
<Field label={label} htmlFor={id} hint={T.analysisPairsHint} className="sm:col-span-2">
|
<Field label={label} htmlFor={id} hint={T.analysisPairsHint} className="sm:col-span-2">
|
||||||
<textarea id={id} className="field min-h-12 text-xs" defaultValue={text} data-eval-analysis-param={field.key}
|
<textarea id={id} className="field min-h-12 text-xs" defaultValue={text} data-eval-analysis-param={key}
|
||||||
onChange={(e) => onChange(e.target.value.split('\n').map((line) => line.split(/[,,、]/).map((x) => x.trim())
|
onChange={(e) => onChange(paramValue('pairs', e.target.value))} />
|
||||||
.filter(Boolean)).filter((pair) => pair.length))} />
|
|
||||||
</Field>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (field.kind === 'column' || field.kind === 'choice') {
|
if (spec.kind === 'basis' || spec.kind === 'grain') {
|
||||||
const options = field.kind === 'column' ? Object.fromEntries(columns.map((c) => [c, c])) : field.choices ?? {}
|
// 和分析卡同两份下拉:空着是缺省(比较方式缺省「和比较期比的变化率」、粒度按期的写法推断),其余每种各有叫法
|
||||||
|
const opts = spec.kind === 'basis' ? A.basis : A.grain
|
||||||
return (
|
return (
|
||||||
<Field label={label} htmlFor={id}>
|
<Field label={label} htmlFor={id}>
|
||||||
<select id={id} className="field" value={value == null ? '' : String(value)} data-eval-analysis-param={field.key}
|
<select id={id} className="field" value={text} data-eval-analysis-param={key}
|
||||||
onChange={(e) => onChange(e.target.value)}>
|
onChange={(e) => onChange(e.target.value)}>
|
||||||
<option value="">{field.kind === 'column' ? T.analysisColumn : T.analysisNone}</option>
|
{Object.entries(opts).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (spec.kind === 'column') {
|
||||||
|
const options = Object.fromEntries(columns.map((c) => [c, c]))
|
||||||
|
return (
|
||||||
|
<Field label={label} htmlFor={id}>
|
||||||
|
<select id={id} className="field" value={text} data-eval-analysis-param={key}
|
||||||
|
onChange={(e) => onChange(e.target.value)}>
|
||||||
|
<option value="">{T.analysisColumn}</option>
|
||||||
{Object.entries(options).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
{Object.entries(options).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 期次、对照组、叫法可以写 {{ schedule.* }} 这类时间变量;整数的格子写了不是整数的字照原样交给服务端校验
|
||||||
return (
|
return (
|
||||||
<Field label={label} htmlFor={id}>
|
<Field label={label} htmlFor={id}>
|
||||||
<input id={id} className={clsx('field', field.kind === 'int' && 'tnum w-24')} value={value == null ? '' : String(value)}
|
<input id={id} className={clsx('field', spec.kind === 'int' && 'tnum w-24')} value={text}
|
||||||
inputMode={field.kind === 'int' ? 'numeric' : undefined} placeholder={field.kind === 'text' ? T.analysisNone : undefined}
|
inputMode={spec.kind === 'int' ? 'numeric' : undefined}
|
||||||
data-eval-analysis-param={field.key}
|
placeholder={ANALYSIS_TEXT.node.paramPlaceholder[key]}
|
||||||
onChange={(e) => onChange(field.kind === 'int' ? (e.target.value.trim() ? Number(e.target.value) : '') : e.target.value)} />
|
data-eval-analysis-param={key}
|
||||||
|
onChange={(e) => onChange(paramValue(spec.kind, e.target.value))} />
|
||||||
</Field>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user