Deploy / deploy (push) Has been cancelled
AI 时代 OJ 设计的第 1 步:给「可信 AC」和学情分析攒数据,本身不判任何事。 - 契约:提交请求加可选的 trace(活跃时长、键入/粘贴/删除字符数、切后台次数等, 只有计数、不含按键内容);写成 .optional().catch(undefined),坏了就当没带, 不让附带数据把提交挡成 400 - 迁移 0016 建 submission_trace,与 submission 一对一、CASCADE;since_prev_ms 由服务端在同一条 INSERT 里算(排掉自身),bigint —— 实测已有 44 天的间隔,int4 装不下 - 后端写 trace 失败只记日志,不影响提交 - 前端 oj/problem/utils/editTrace.ts 是模块单例(扩展对象不能过 Pinia 的响应式代理), 只数带 userEvent 的事务:格式化回写 / 载入草稿 / 协作对方的改动天然排除; closeBrackets 越过右括号时是原样替换,按 no-op 跳过。比赛编辑器同样挂上 - 实跑:后端四种请求、前端浏览器里键入/粘贴/删除/setCode 回写/切后台/提交后清零, 计数与预期逐项一致 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
452 lines
20 KiB
TypeScript
452 lines
20 KiB
TypeScript
import { z } from "zod"
|
||
|
||
import { paginatedSchema } from "./common"
|
||
import { judgeStatusSchema, type JudgeStatus } from "./judge-status"
|
||
import { problemLanguageSchema } from "./language"
|
||
|
||
/**
|
||
* 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。**
|
||
*
|
||
* 这里曾经是一组 zod schema,按生产库实测的键集收紧过,结果是 124192 条提交里有
|
||
* 9163 条(RE 8480/8480、TLE 338/338、MLE 1/1 全中)被判成不符:沙箱在非正常退出
|
||
* 的测试点上写 `output_md5: null`,而 SQL 判题(`judge/sql/engine.ts` 的 CaseResult)
|
||
* 压根没有 `output` 这个键、`error_message` 通过时是 null。收紧当时只对了键集合,
|
||
* 没对空值。
|
||
*
|
||
* 更糟的是失败方式:`info` 当时是 `union([完整形状, z.object({})])`,对不上的一律
|
||
* 落进第二支被剥成 `{}` 且 parse 成功 —— 管理员的测试点表格**静默消失**。
|
||
*
|
||
* 结论:JSONB 的形状真相在**写入侧**(判题机、`judge/run.ts`),在读出侧再校验一遍
|
||
* 只会在两边分叉时丢数据。所以 `info` 回到 `z.unknown()`,形状以下面的 TS 类型
|
||
* 描述,取值处由 `submissionCaseResults()` 做一次真正需要的运行时判断(有没有
|
||
* data 数组)。**改这里的字段时对着判题机改,不要对着采样出来的键集改。**
|
||
*
|
||
* 键名是**判题沙箱定的 snake_case**,不要跟着响应字段一起改。
|
||
*/
|
||
export interface JudgeCaseResult {
|
||
error: number
|
||
memory: number
|
||
/** SQL 判题没有这个键 */
|
||
output?: string | null
|
||
result: JudgeStatus
|
||
signal: number
|
||
cpu_time: number
|
||
exit_code: number
|
||
real_time: number
|
||
test_case: string
|
||
/** 非正常退出的测试点上是 null */
|
||
output_md5: string | null
|
||
/** SQL 判题会带上中文原因(通过的测试点是 null),沙箱判题没有这个键 */
|
||
error_message?: string | null
|
||
score?: number
|
||
}
|
||
|
||
/**
|
||
* `info` 的完整形状。实际取值还有第三种:**空对象** —— 后端对非管理员下发
|
||
* `info: {}`(`routes/submission.ts` 的 `full ? row.submission.info : {}`),
|
||
* 也是插入待判提交时的初值。所以调用方不能直接 `.data`。
|
||
*/
|
||
export interface JudgeInfo {
|
||
err: string | null
|
||
data: JudgeCaseResult[] | null
|
||
}
|
||
|
||
/**
|
||
* 判题产出的统计(`submission.statistic_info` 的 JSONB 原文)。
|
||
*
|
||
* 五个键全部可选,依据是生产库实测的出现次数:time_cost / memory_cost 各 112097、
|
||
* score 3993、err_info 3153、ast_results 56,另有 27 条空对象。
|
||
*
|
||
* 用 `looseObject`:所有键可选 + 不剥未知键 = **对任何对象都不会失败、也不丢字段**,
|
||
* 它在这里的作用是给前端一个能读 `err_info` 的类型,而不是一道闸门。判题产物的
|
||
* 闸门在写入侧,理由见上面 `JudgeCaseResult`。
|
||
*/
|
||
export const statisticInfoSchema = z.looseObject({
|
||
score: z.number().optional(),
|
||
/** 判题机写进 statistic_info 的错误文本,教师面板的「最近一条错在哪」也读它 */
|
||
err_info: z.string().optional(),
|
||
time_cost: z.number().optional(),
|
||
memory_cost: z.number().optional(),
|
||
ast_results: z
|
||
.array(
|
||
z.object({
|
||
description: z.string(),
|
||
passed: z.boolean(),
|
||
/** count_* 规则实际数到的次数,判题机只在这两个引擎上写 */
|
||
actual: z.number().optional(),
|
||
}),
|
||
)
|
||
.optional(),
|
||
})
|
||
|
||
/**
|
||
* 编辑过程的聚合信号,落进 `submission_trace`。**只有计数,不含任何按键内容。**
|
||
*
|
||
* 口径是「自上次提交以来」的增量(前端每次提交成功后清零,切题也清零),
|
||
* 所以同一道题连交几次,每条提交各记各的那一段。
|
||
*
|
||
* 全部来自客户端,**可以伪造** —— 这是接受了的:它只用来给「可信 AC」加权、
|
||
* 给老师提示「建议关注」,不单独判任何事。服务端自己算的间隔另见 `since_prev_ms`。
|
||
*/
|
||
export const submissionTraceSchema = z.object({
|
||
/** 活跃编辑时长:相邻两次编辑间隔不超过 60 秒才累加,页面不可见时不计 */
|
||
activeMs: z.number().int().min(0).max(1e8),
|
||
/** 打开这道题(或上次提交)到这次提交的墙钟时长 */
|
||
sinceOpenMs: z.number().int().min(0).max(1e9),
|
||
/** 键入、输入法上屏、补全插入的字符数 */
|
||
typedChars: z.number().int().min(0).max(1e7),
|
||
/** 粘贴、从外部拖入的字符数 */
|
||
pastedChars: z.number().int().min(0).max(1e7),
|
||
pasteCount: z.number().int().min(0).max(1e5),
|
||
/** 单次最大粘贴的字符数 */
|
||
maxPaste: z.number().int().min(0).max(1e7),
|
||
deletedChars: z.number().int().min(0).max(1e7),
|
||
/** 页面切到后台的次数(切标签页、切窗口、最小化)。只作辅助,别单独拿来说事 */
|
||
blurCount: z.number().int().min(0).max(1e5),
|
||
/** 这一段开始时编辑器里已有的字符数(本地草稿 / 模板 / 上次提交后的代码) */
|
||
initialLen: z.number().int().min(0).max(1e7),
|
||
/** 提交时这道题正在课堂协作中。老师替学生交的那条也会是 true,统计时要排掉 */
|
||
collab: z.boolean(),
|
||
})
|
||
|
||
export const createSubmissionRequestSchema = z.object({
|
||
problemId: z.number().int().positive(),
|
||
/**
|
||
* 提交的语言。用题目语言的联合而不是 `z.string()` —— 学生能选的语言就是题目
|
||
* `languages` 里列出的那些,写宽松了的话,前端把语言拼错(`"C++"`、`"python3"`
|
||
* 大小写)会一路走到判题机才以 `Unsupported judge language` 报系统错误,
|
||
* 学生看到的是「系统错误」而不是「语言不对」。
|
||
*/
|
||
language: problemLanguageSchema,
|
||
code: z
|
||
.string()
|
||
.min(1)
|
||
.max(1024 * 1024),
|
||
contestId: z.number().int().positive().optional(),
|
||
/**
|
||
* 来源题单。学生从 `/problemset/:id/problem/:pid` 那个入口提交时前端带上,
|
||
* 后端落进 `submission.problemset_id`,提交列表据此标出「这条是刷题单刷出来的」。
|
||
*
|
||
* 只是**来源标记**,不参与判题、也不参与题单进度记账 —— 进度由判完之后的
|
||
* `recordSolvedProblem` 记进所有已加入且含这道题的题单,和从哪个入口进来无关。
|
||
* 所以这里带错了顶多是标记不准,不会影响成绩。
|
||
*/
|
||
problemSetId: z.number().int().positive().optional(),
|
||
/**
|
||
* 编辑过程信号,见 submissionTraceSchema。**坏了就当没带**(`.catch`):
|
||
* 整个请求体是一把 safeParse,这里要是能 400,一份附带的统计数据就能挡住
|
||
* 学生交作业。刷新过页面、老版本前端、脚本提交都会没有它,那是「无数据」,
|
||
* 不是「可疑」。
|
||
*/
|
||
trace: submissionTraceSchema.optional().catch(undefined),
|
||
})
|
||
|
||
export const createSubmissionResponseSchema = z.object({
|
||
submissionId: z.string(),
|
||
})
|
||
|
||
export const submissionDetailSchema = z.object({
|
||
id: z.string(),
|
||
createTime: z.string(),
|
||
userId: z.number().int(),
|
||
username: z.string(),
|
||
code: z.string(),
|
||
result: judgeStatusSchema,
|
||
/** 判题机原文;未判完或非管理员看时为 `{}`,见 JudgeInfo 的注释 */
|
||
info: z.unknown(),
|
||
language: problemLanguageSchema,
|
||
statisticInfo: statisticInfoSchema,
|
||
contestId: z.number().int().nullable(),
|
||
problemId: z.number().int(),
|
||
/**
|
||
* 题目的展示编号(problem._id)。**独立的 /submission/:id 页面要靠它** ——
|
||
* 那条路由只喂 submissionID,组件拿不到 display id,而「复制回到题目」要用它
|
||
* 拼路由。原来只给内部数字 id,于是那个按钮在这条路由上一点就抛
|
||
* `Missing required param "problemID"`。
|
||
*/
|
||
problemDisplayId: z.string(),
|
||
showLink: z.boolean(),
|
||
/**
|
||
* 通过了几个测试点,给学生看「离 AC 还差多远」。`info` 只给管理员(每个点带
|
||
* output_md5),所以这里只算出两个数下发,不放开原文。
|
||
*
|
||
* 为 null 的情形:比赛提交(ACM 只报对错,多给通过数等于变相放水,和 AI 提示同口径)、
|
||
* SQL 题(`judge/run.ts` 遇到被杀的测试点会 break,total 偏小)、没有逐点结果
|
||
* (待判、编译失败)。
|
||
*/
|
||
caseSummary: z
|
||
.object({
|
||
passed: z.number().int(),
|
||
total: z.number().int(),
|
||
})
|
||
.nullable(),
|
||
})
|
||
|
||
/**
|
||
* 内嵌在别处(目前只有站内信)的提交对象。对齐旧后端的
|
||
* `SubmissionSafeModelSerializer(exclude=("info", "contest", "ip"))` ——
|
||
* 这些键**根本不出现**,而不是出现但值为空。(`ip` 已随 IP 功能整体删除。)
|
||
*
|
||
* 独立成一个 schema 而不是复用 submissionDetailSchema 传空值:形状一致了,
|
||
* 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这些字段。
|
||
*/
|
||
export const embeddedSubmissionSchema = submissionDetailSchema
|
||
// problemDisplayId 也要去掉:下面的 problem 就是它,同一个值留两份,
|
||
// 而路由只填了 problem —— 这里漏 omit 的那阵子,凡是收到过站内信的人
|
||
// 打开消息页都是 500(parse 抛在缺失的 problemDisplayId 上,列表为空时才碰巧不炸)。
|
||
.omit({
|
||
info: true,
|
||
contestId: true,
|
||
problemId: true,
|
||
problemDisplayId: true,
|
||
caseSummary: true,
|
||
})
|
||
// 旧 SubmissionSafeModelSerializer 里 problem 是
|
||
// `SlugRelatedField(slug_field="_id")`,即**展示用题号**而非数字主键。
|
||
// 站内信页面拿它拼 `/problem/<题号>` 链接,给数字 id 会拼出打不开的地址。
|
||
.extend({ problem: z.string() })
|
||
|
||
/**
|
||
* 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result /
|
||
* status 决定是继续等还是去拉详情。
|
||
*
|
||
* 这里曾经还带着 time_cost / memory_cost / err_info —— 从 statistic_info 原样
|
||
* 抄一份出来,前端一处都没读过。耗时和错误信息在提交详情里本来就有,判完了去
|
||
* 拉一次就是了,不必让推送顺带背一份 JSONB 的形状。
|
||
*/
|
||
export const submissionUpdateSchema = z.object({
|
||
type: z.literal("submission_update"),
|
||
submissionId: z.string(),
|
||
result: judgeStatusSchema,
|
||
status: z.enum(["pending", "judging", "finished", "error"]),
|
||
score: z.number().optional(),
|
||
})
|
||
|
||
export const submissionListItemSchema = z.object({
|
||
id: z.string(),
|
||
problem: z.string(),
|
||
problemTitle: z.string(),
|
||
showLink: z.boolean(),
|
||
createTime: z.string(),
|
||
userId: z.number().int(),
|
||
username: z.string(),
|
||
result: judgeStatusSchema,
|
||
language: problemLanguageSchema,
|
||
statisticInfo: statisticInfoSchema,
|
||
/**
|
||
* 来源题单,非题单入口提交的为 null。比赛提交恒为 null(比赛题不会进题单)。
|
||
* 历史提交里只有「当年首次 AC 那一条」有值 —— 迁移 0007 从 problemset_submission
|
||
* 回填的就是这些,其余老提交无从判断入口,一律留空。
|
||
*/
|
||
problemSet: z.object({ id: z.number().int(), title: z.string() }).nullable(),
|
||
})
|
||
|
||
export const submissionListSchema = paginatedSchema(submissionListItemSchema)
|
||
|
||
/**
|
||
* **一条都没交**的学生。`realName` 是从用户名里剥掉 `ks<班级号>` 前缀后剩下的那一段,
|
||
* 不是 user.real_name 列 —— 与 F2「真名默认不下发」不冲突:这里只有教师能看到,
|
||
* 且教师面板的用途正是点名谁没做。
|
||
*
|
||
* 注意它不是「未完成」的全部:交了但一次没对的学生在 `dataAttempted` 里。
|
||
*/
|
||
export const unacceptedStudentSchema = z.object({
|
||
username: z.string(),
|
||
realName: z.string(),
|
||
})
|
||
|
||
/**
|
||
* **交了但一次没对**的学生。这批人原来两栏都不在 —— 不在「完成人数」(没 AC),
|
||
* 也不在「未完成」名单(那一栏只收一条没交的),于是课堂上最该去看一眼的人
|
||
* 反而从屏幕上消失了。`submissionCount` 是窗口内的提交次数,教师据此判断
|
||
* 「卡了多久」。
|
||
*/
|
||
export const attemptedStudentSchema = unacceptedStudentSchema.extend({
|
||
submissionCount: z.number().int(),
|
||
/**
|
||
* 已经解决的题数。查多道题时这一栏里混着「一道没对」和「三道做出两道」两种人,
|
||
* 差几道决定了老师先管谁 —— 所以名字后面要缀 `2/3`。
|
||
*/
|
||
solvedCount: z.number().int(),
|
||
/**
|
||
* 最近一条提交错在哪。教师点名字就能看到「是编译错了还是答案错了」,
|
||
* 不必再切去提交列表翻这个人。`error` 是判题机写进 statistic_info 的 err_info,
|
||
* 已截断;没有错误文本(比如答案错误那种)时为 null。
|
||
*/
|
||
lastFailure: z
|
||
.object({
|
||
id: z.string(),
|
||
/** 题目的展示编号,用来告诉老师错在哪道题 */
|
||
problem: z.string(),
|
||
result: judgeStatusSchema,
|
||
error: z.string().nullable(),
|
||
})
|
||
.nullable(),
|
||
})
|
||
|
||
export const submissionStatisticsUserSchema = z.object({
|
||
username: z.string(),
|
||
className: z.string().nullable(),
|
||
submissionCount: z.number().int(),
|
||
/** 通过的**提交条数**。correctRate 的分子就是它 */
|
||
acceptedCount: z.number().int(),
|
||
/**
|
||
* 解决的**题数**(同一道题重复 AC 只算一道)。表格「已解决」那一列显示的是它 ——
|
||
* 不指定题号查「这节课全班」时,条数和题数能差出好几倍。
|
||
*/
|
||
solvedCount: z.number().int(),
|
||
/**
|
||
* 「答案对了但语法没按要求写」且**最后也没改对**的题数。这些题算在 solvedCount 里
|
||
* (AST_CHECK_FAILED 全站都算通过),单列出来只是让教师看得见教学上没达标的那几个。
|
||
*/
|
||
astOnlyCount: z.number().int(),
|
||
/** 这个人还在判题队列里的条数。`submissionCount` 含它,`correctRate` 的分母不含 */
|
||
judgingCount: z.number().int(),
|
||
// 百分比数值,不带 %。旧后端返回 "85.5%" 字符串,展示格式化交给前端。
|
||
correctRate: z.number(),
|
||
/**
|
||
* 这个人在本次查询的口径下做完了没有(查了 N 道题就要 N 道都解决)。
|
||
*
|
||
* `data` 里**没做完的人也在**,教师才能在同一张表里展开看他错在哪;「完成人数」
|
||
* 和完成度算的是 `done` 为真的那些,不是 `data.length`。
|
||
*/
|
||
done: z.boolean(),
|
||
})
|
||
|
||
/**
|
||
* 展开行的明细,**按需拉**(GET /submissions/statistics/items)。
|
||
*
|
||
* 原来是随统计一起给每个人各带一份,可表格一次只展开一行 —— 生产快照上那是
|
||
* 4.9 万行没人看的数据。`truncated` 为真时前端要说明「只显示最近 N 条」,
|
||
* 免得老师以为这人就交了这么多。
|
||
*/
|
||
export const submissionStatisticsItemsSchema = z.object({
|
||
/**
|
||
* 展开某个学生时列出他这段时间的提交。**带上题目**:一节课里学生往往在好几道题
|
||
* 之间来回跳,一串只有编号的按钮看不出他卡在哪一道 —— 前端按题目分组展示。
|
||
*
|
||
* 字段名沿用 submissionListItemSchema 的口径:`problem` 是展示用题号(problem._id),
|
||
* `problemTitle` 是标题。
|
||
*/
|
||
items: z.array(
|
||
z.object({
|
||
id: z.string(),
|
||
result: judgeStatusSchema,
|
||
createTime: z.string(),
|
||
problem: z.string(),
|
||
problemTitle: z.string(),
|
||
}),
|
||
),
|
||
truncated: z.boolean(),
|
||
})
|
||
|
||
export const submissionStatisticsSchema = z.object({
|
||
submissionCount: z.number().int(),
|
||
acceptedCount: z.number().int(),
|
||
/**
|
||
* 还没判完的条数(PENDING / JUDGING)。`submissionCount` 把它算在内,
|
||
* `correctRate` 的分母不算 —— 全班同时交卷的那几秒,分母涨了分子没涨,
|
||
* 正确率会凭空掉一截。下发它是为了让教师看得出「那几条还在判」。
|
||
*/
|
||
judgingCount: z.number().int(),
|
||
correctRate: z.number(),
|
||
// 花名册人数(未禁用的普通用户)。**只有这一个分母下发**:完成度由前端算,
|
||
// 因为「请假隐藏」会把请假的人从分母里减掉,那是后端不知道的浏览器本地状态。
|
||
personCount: z.number().int(),
|
||
/** 窗口里交过东西的所有人(做没做完看 `done`),按提交数倒序 */
|
||
data: z.array(submissionStatisticsUserSchema),
|
||
/** 一条都没交的(花名册里的人减去有提交的人) */
|
||
dataUnaccepted: z.array(unacceptedStudentSchema),
|
||
/**
|
||
* 交了但没做完的(一道没对,或者查三道只做出两道)。
|
||
*
|
||
* 传了用户名时按花名册取,和 dataUnaccepted 同一个范围;不传用户名时没有花名册,
|
||
* 退回「窗口内有提交但没做完的全部普通学生」—— 否则这批人两栏都不在,看起来
|
||
* 就像统计只认成功的提交。dataUnaccepted 没有花名册就真的算不出来,仍然为空。
|
||
*/
|
||
dataAttempted: z.array(attemptedStudentSchema),
|
||
})
|
||
|
||
/**
|
||
* 提交列表那颗「今日提交数」标签点开之后的统计弹框(GET /submissions/today-statistics)。
|
||
*
|
||
* **公开接口,只下发聚合数** —— 没有用户名、没有代码、没有隐藏题目的标题,学生和
|
||
* 匿名访客看到的和教师一样。教师那套按班级/按题号钻取的口径在
|
||
* `submissionStatisticsSchema`,两者不是一回事,别把这个当它的简版去加字段。
|
||
*
|
||
* 口径跟着那颗标签走:**东八区今天、非比赛提交、不分语言**(语言分布就是
|
||
* `languages` 这张表本身)。
|
||
*/
|
||
export const todaySubmissionStatisticsSchema = z.object({
|
||
total: z.number().int(),
|
||
/** 通过的条数,含 AST_CHECK_FAILED(那也是答案对了) */
|
||
accepted: z.number().int(),
|
||
/**
|
||
* 还没判完的条数(PENDING / JUDGING)。`total` 把它算在内,`correctRate` 的分母
|
||
* 不算 —— 全班同时交卷的那几秒,分母涨了分子没涨,正确率会凭空掉一截。
|
||
*/
|
||
judging: z.number().int(),
|
||
correctRate: z.number(),
|
||
/** 今天交过东西的人数,按 user_id 去重 */
|
||
userCount: z.number().int(),
|
||
/** 按东八区钟点分的 24 个桶,**下标就是钟点**,没有提交的钟点是 0 */
|
||
hours: z.array(z.number().int()).length(24),
|
||
/** 按语言,提交数倒序。零提交的语言不在表里 */
|
||
languages: z.array(
|
||
z.object({ language: problemLanguageSchema, count: z.number().int() }),
|
||
),
|
||
/** 按判题结果,条数倒序 */
|
||
results: z.array(
|
||
z.object({ result: judgeStatusSchema, count: z.number().int() }),
|
||
),
|
||
/**
|
||
* 今天最热的几道题,提交数倒序,最多 10 道。
|
||
* **只含公开可见的题目** —— 这个接口不需要登录,不能拿它探未发布题目的标题。
|
||
*/
|
||
problems: z.array(
|
||
z.object({
|
||
problem: z.string(),
|
||
problemTitle: z.string(),
|
||
count: z.number().int(),
|
||
acceptedCount: z.number().int(),
|
||
}),
|
||
),
|
||
})
|
||
|
||
export const formatCodeRequestSchema = z.object({
|
||
code: z.string().max(1024 * 1024),
|
||
language: z.enum(["python", "c", "cpp", "sql"]),
|
||
})
|
||
|
||
export const formatCodeResponseSchema = z.object({ code: z.string() })
|
||
|
||
export type StatisticInfo = z.infer<typeof statisticInfoSchema>
|
||
export type SubmissionTrace = z.infer<typeof submissionTraceSchema>
|
||
export type CreateSubmissionRequest = z.infer<
|
||
typeof createSubmissionRequestSchema
|
||
>
|
||
export type SubmissionDetail = z.infer<typeof submissionDetailSchema>
|
||
export type SubmissionUpdate = z.infer<typeof submissionUpdateSchema>
|
||
export type SubmissionStatistics = z.infer<typeof submissionStatisticsSchema>
|
||
export type TodaySubmissionStatistics = z.infer<
|
||
typeof todaySubmissionStatisticsSchema
|
||
>
|
||
export type SubmissionStatisticsUser = z.infer<
|
||
typeof submissionStatisticsUserSchema
|
||
>
|
||
export type SubmissionStatisticsItems = z.infer<
|
||
typeof submissionStatisticsItemsSchema
|
||
>
|
||
export type UnacceptedStudent = z.infer<typeof unacceptedStudentSchema>
|
||
export type AttemptedStudent = z.infer<typeof attemptedStudentSchema>
|
||
|
||
export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
|
||
export type SubmissionList = z.infer<typeof submissionListSchema>
|
||
export type EmbeddedSubmission = z.infer<typeof embeddedSubmissionSchema>
|
||
export type CreateSubmissionResponse = z.infer<
|
||
typeof createSubmissionResponseSchema
|
||
>
|
||
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>
|
||
|
||
export type FormatCodeRequest = z.infer<typeof formatCodeRequestSchema>
|