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:
2026-08-25 23:15:52 -06:00
parent 3f55d231c3
commit f919d41199
6 changed files with 75 additions and 48 deletions

View File

@@ -13,6 +13,7 @@ import type {
ProblemDetail,
ProblemDifficulty,
JudgeStatus,
AstRules,
CreateAnnouncementRequest,
} from "@oj2/contract"
@@ -111,7 +112,6 @@ export type { ProblemTestCaseScore as Testcase } from "@oj2/contract"
/**
* 题目详情。以契约的 ProblemDetail 为准,只在这里补两处前端自己的窄化:
* - `languages` / `template` 的键窄化成 LANGUAGE组件按语言查模板要靠它
* - `astRules` 契约里是 unknown这里给出组件实际读的形状
*/
export type Problem = Omit<ProblemDetail, "languages" | "template"> & {
languages: LANGUAGE[]
@@ -122,15 +122,11 @@ export type Problem = Omit<ProblemDetail, "languages" | "template"> & {
answers?: { language: LANGUAGE; code: string }[]
}
export type AstRules = {
[key: string]: {
engine: string
target?: string
min?: number
max?: number
message: string
}[]
}
/**
* AST 代码要求。原来这里手抄的那份少了 label / exact / outer / inner ——
* 编辑器写得出 label 和 exact判题机也认只有这个类型不认。形状在契约里。
*/
export type { AstRule, AstRules, AstRuleEngine } from "@oj2/contract"
export type {
ProblemDetail,