refactor(契约): AST 代码要求收进契约,三份各写各的合成一份
同一个形状原来在三个地方各定义了一份,三份都不一样: apps/api/src/judge/ast.ts engine target outer inner label message exact min max apps/web/src/utils/types.ts engine target message min max AstRulesEditor.vue(本地) engine target label message exact min max ProblemContent.vue(本地) engine target label message exact min max 判题机那份是真的(evaluateRule 按 engine 分支读哪几个字段),契约里则压根没有, `astRules: z.unknown()`。后果是后台编辑器写得出 label 和 exact、判题机也认, 但 utils/types.ts 那个类型描述不了它们 —— 生产库 12 道带 AST 规则的题里, 15 条规则带 label、6 条带 exact,全都在类型之外。 现在契约里一份 astRuleSchema,四处都指向它。engine 收成枚举,列全判题机 实现的十种。**存量数据核过**:12 道题逐条过新 schema,12/12 通过。 顺带三件: - `astRules` 的响应和请求 schema 从 z.unknown() 换成 astRulesSchema。之前 engine 写错一个字母能存进去,判题机 evaluateRule 走 `default: return null` 静默跳过 —— 老师设了规则、规则不生效、没有任何提示。现在保存时就 400, 错误信息把十个合法值列出来。 - judge/run.ts 的 astRulesForLanguage 原来是整片 `rules as AstRule[]` 硬转, 改成逐条 safeParse:认不出的丢掉那一条,行为和 evaluateRule 的 default 分支 一致,只是提前到读取处,也不再骗类型系统。 - 判题机实现了 must_have_nesting,但后台编辑器没有对应选项,目前只能手工造 数据才用得上。枚举里留着并加了注释,没有顺手去补 UI(那是加功能不是清理)。 ## 验证 tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。 起服务实打: - 把生产库那条带 label/exact 的规则种进本地库,后台题目详情 200、字段齐全; 原样 PUT 回去 200,库里 label 和 exact 都在(旧类型描述不了的那两个)。 - engine 传 "must_do_magic" → 400,错误信息列出十个合法值。 - 直接调 checkAst:三条规则(两条合法 + 一条 engine 认不出)进去,判题机收下 两条;两个 if/else 的代码 passed=true,一个的 passed=false,描述文案 「if 条件 出现 2 次」正确用上了 label。 冒烟改动已还原(problem 2 的 ast_rules 复位成 null,测试标签删掉)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ export const createAnnouncementRequestSchema = z.object({
|
||||
|
||||
export const updateAnnouncementRequestSchema = createAnnouncementRequestSchema
|
||||
|
||||
export type AstRuleEngine = z.infer<typeof astRuleEngineSchema>
|
||||
export type AstRule = z.infer<typeof astRuleSchema>
|
||||
export type AstRules = z.infer<typeof astRulesSchema>
|
||||
export type ProblemSample = z.infer<typeof problemSampleSchema>
|
||||
export type ProblemAnswer = z.infer<typeof problemAnswerSchema>
|
||||
export type ProblemTestCaseScore = z.infer<typeof problemTestCaseScoreSchema>
|
||||
@@ -566,6 +569,50 @@ export const problemTestCaseScoreSchema = z.object({
|
||||
score: z.coerce.number().int().min(0),
|
||||
})
|
||||
|
||||
/**
|
||||
* AST 代码要求。同一个形状原来在**三个地方**各写了一份,三份都不一样:
|
||||
* apps/api/src/judge/ast.ts 的 AstRule(判题机真读的那份,九个字段)、
|
||||
* apps/web/src/utils/types.ts 的 AstRules(少了 label / exact / outer / inner)、
|
||||
* AstRulesEditor.vue 里的本地 AstRule(少了 outer / inner)。
|
||||
* 编辑器写得出 label / exact,题目类型却描述不了它们。现在以这里为准。
|
||||
*
|
||||
* 除 engine 外全部可选:判题机每条规则只读自己那几个字段
|
||||
* (见 ast.ts 的 evaluateRule),缺了就走默认文案。
|
||||
*/
|
||||
export const astRuleEngineSchema = z.enum([
|
||||
"must_exist_node",
|
||||
"must_not_exist_node",
|
||||
"count_node",
|
||||
"must_call_function",
|
||||
"must_not_call_function",
|
||||
"count_function_call",
|
||||
"must_call_method",
|
||||
"must_not_call_method",
|
||||
"must_use_operator",
|
||||
// 判题机实现了,但后台编辑器还没有对应的选项,目前只能手工造数据用上
|
||||
"must_have_nesting",
|
||||
])
|
||||
|
||||
export const astRuleSchema = z.object({
|
||||
engine: astRuleEngineSchema,
|
||||
/** 检查目标:节点类型 / 函数名 / 方法名 / 运算符,按 engine 而定 */
|
||||
target: z.string().optional(),
|
||||
/** must_have_nesting 专用:外层、内层节点 */
|
||||
outer: z.string().optional(),
|
||||
inner: z.string().optional(),
|
||||
/** 展示用的中文名,缺省回落到 target */
|
||||
label: z.string().optional(),
|
||||
/** 自定义提示。生产库里存的是空串而不是缺键,判题机按 `||` 回落到默认文案 */
|
||||
message: z.string().optional(),
|
||||
/** count_* 引擎的次数约束 */
|
||||
exact: z.number().int().optional(),
|
||||
min: z.number().int().optional(),
|
||||
max: z.number().int().optional(),
|
||||
})
|
||||
|
||||
/** 按语言分组:`{ Python3: [...], C: [...] }`,键是 languages 里的语言名 */
|
||||
export const astRulesSchema = z.record(z.string(), z.array(astRuleSchema))
|
||||
|
||||
/** 后台题目详情:包含 oj 侧永不下发的 answers / testCase* / astRules */
|
||||
export const adminProblemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -599,7 +646,7 @@ export const adminProblemSchema = z.object({
|
||||
showFlowchart: z.boolean(),
|
||||
mermaidCode: z.string().nullable(),
|
||||
flowchartHint: z.string().nullable(),
|
||||
astRules: z.unknown(),
|
||||
astRules: astRulesSchema.nullable(),
|
||||
answers: z.array(problemAnswerSchema),
|
||||
prompt: z.string().nullable(),
|
||||
sqlConfig: sqlConfigSchema.nullable(),
|
||||
@@ -631,7 +678,7 @@ export const createProblemRequestSchema = z.object({
|
||||
showFlowchart: z.boolean().default(false),
|
||||
mermaidCode: z.string().nullable().default(null),
|
||||
flowchartHint: z.string().nullable().default(null),
|
||||
astRules: z.unknown().default(null),
|
||||
astRules: astRulesSchema.nullable().default(null),
|
||||
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
||||
// `BooleanField(default=False)` 一致
|
||||
sqlConfig: sqlConfigSchema
|
||||
|
||||
Reference in New Issue
Block a user