@exercode/problem-utils 4.3.4 → 4.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/checkAllProblems.cjs +1 -1
- package/dist/cli/checkAllProblems.cjs.map +1 -1
- package/dist/cli/checkAllProblems.js +1 -1
- package/dist/cli/checkAllProblems.js.map +1 -1
- package/dist/cli/exercodeProblem.cjs.map +1 -1
- package/dist/cli/exercodeProblem.js.map +1 -1
- package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
- package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
- package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
- package/dist/helpers/findModelAnswerDirs.js.map +1 -1
- package/dist/helpers/javaJudge.cjs.map +1 -1
- package/dist/helpers/javaJudge.js.map +1 -1
- package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
- package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
- package/dist/helpers/readOutputFiles.cjs.map +1 -1
- package/dist/helpers/readOutputFiles.js.map +1 -1
- package/dist/helpers/resolveCwds.cjs +1 -1
- package/dist/helpers/resolveCwds.cjs.map +1 -1
- package/dist/helpers/resolveCwds.js +1 -1
- package/dist/helpers/resolveCwds.js.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
- package/dist/helpers/sourceCodeGrammars.cjs.map +1 -1
- package/dist/helpers/sourceCodeGrammars.js.map +1 -1
- package/dist/helpers/spawnWithLimits.cjs.map +1 -1
- package/dist/helpers/spawnWithLimits.js.map +1 -1
- package/dist/learningMaterial/validateContest.cjs.map +1 -1
- package/dist/learningMaterial/validateContest.js.map +1 -1
- package/dist/learningMaterial/validateCourse.cjs.map +1 -1
- package/dist/learningMaterial/validateCourse.js.map +1 -1
- package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
- package/dist/learningMaterial/validateMaterial.js.map +1 -1
- package/dist/learningMaterial/validateProblem.cjs.map +1 -1
- package/dist/learningMaterial/validateProblem.js.map +1 -1
- package/dist/presets/command.cjs +1 -1
- package/dist/presets/command.cjs.map +1 -1
- package/dist/presets/command.js +1 -1
- package/dist/presets/command.js.map +1 -1
- package/dist/presets/evaluation.cjs +1 -1
- package/dist/presets/evaluation.cjs.map +1 -1
- package/dist/presets/evaluation.js +1 -1
- package/dist/presets/evaluation.js.map +1 -1
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.js.map +1 -1
- package/dist/presets/springBoot.cjs.map +1 -1
- package/dist/presets/springBoot.js.map +1 -1
- package/dist/presets/stdio.cjs.map +1 -1
- package/dist/presets/stdio.js.map +1 -1
- package/dist/presets/tomcat.cjs.map +1 -1
- package/dist/presets/tomcat.js.map +1 -1
- package/package.json +23 -26
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateMaterial.js","names":["parseYaml","QUESTION_CODE_BLOCK_REGEX","QUESTION_CODE_BLOCK_OPENER_REGEX","PROBLEM_LINK_REGEX","TURTLE_GRAPHICS_QUESTION_LINK_REGEX","PROBLEM_ASSIGNMENT_REGEX","QUESTION_LINK_REGEX","TURTLE_GRAPHICS_ASSIGNMENT_REGEX","STANDALONE_CHAT_MARKER_REGEX","validateMaterialFile","errors","result","materialId","basename","materialFilePath","LEARNING_MATERIAL_ID_REGEX","markdown","readFile","attributes","body","parseFrontmatter","error","getErrorMessage","parsedFrontmatter","materialFrontmatterSchema","formatZodIssues","frontmatter","questionsInCodeBlocks","parseQuestionCodeBlocks","questionBlockIds","question","questions","validateQuestion","reportDuplicateIds","bodyWithoutQuestionBlocks","collectLinkedIds","linkedTurtleGraphicsQuestionIds","problemIds","problem","reportForeignProblemCourseIds","options","availableProblemIds","collectProblemIdsFromDirectory","reportDanglingProblemReferences","reportExcessChatMarkers","validateSubmissionPeriods","mergeSubmissionPeriods","linkRegex","match","id","own","courseDefaults","chatMarkerCount","courseId","problems","matches","leftoverOpenerCount","blockIndex","yamlText","\"yaml\"","rawQuestion","parse","parsed","questionInCodeBlockSchema","isSurvey","answerFieldName","isAnswerDefined","answerIndices","answerIndex","answerPattern","answerRegExp","rawAnswerRegExp","periods","earlierName","laterName","earlierValue","laterValue","ownPeriods"],"sources":["../../src/learningMaterial/validateMaterial.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n LEARNING_MATERIAL_ID_REGEX,\n materialFrontmatterSchema,\n questionInCodeBlockSchema,\n type MaterialQuestion,\n} from './schemas.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\n// Same patterns the judge uses to extract embedded questions and problem links from material bodies.\n// The block regex intentionally requires LF line endings and a trailing newline after the closing\n// fence, exactly like the judge parser; blocks it misses are reported by detecting leftover openers.\nconst QUESTION_CODE_BLOCK_REGEX = /(?:^|\\n)(?<fence>`{3,}|~{3,})ya?ml +question\\n(?<yaml>[\\s\\S]+?)\\n\\k<fence>\\n/g;\n// Strictly laxer than the parser regex (tolerates CRLF, indentation, and stray whitespace) so\n// question-looking blocks the judge parser would silently skip surface as leftover openers.\nconst QUESTION_CODE_BLOCK_OPENER_REGEX = /^[ \\t]*(?:`{3,}|~{3,})[ \\t]*ya?ml[ \\t]+question[ \\t]*\\r?$/gm;\nconst PROBLEM_LINK_REGEX = /\\[.*?]\\(problems\\/([0-9_a-z-]+?)\\)/g;\nconst TURTLE_GRAPHICS_QUESTION_LINK_REGEX = /\\[.*?]\\(turtle-graphics-questions\\/([0-9_a-z-]+?)\\)/g;\n// Exercode's duplicate-assignment check scans the body with these patterns, whose link text may\n// span lines (unlike the judge's reference collection above).\nconst PROBLEM_ASSIGNMENT_REGEX = /\\[[^\\]]*]\\(problems\\/([0-9_a-z-]+)\\)/g;\nconst QUESTION_LINK_REGEX = /@\\[question]\\(([0-9_a-z-]+)\\)/g;\nconst TURTLE_GRAPHICS_ASSIGNMENT_REGEX = /\\[[^\\]]*]\\(turtle-graphics-questions\\/([0-9_a-z-]+)\\)/g;\nconst CHAT_MARKER = '<!-- chat -->';\n// exercode only counts the marker when it stands alone on a line, so fenced or inline mentions don't count.\nconst STANDALONE_CHAT_MARKER_REGEX = /(^|\\r?\\n)<!-- chat -->[ \\t]*(?=\\r?\\n|$)/g;\n\nexport interface SubmissionPeriods {\n submissionOpenedAt?: string;\n submissionSoftClosedAt?: string;\n submissionHardClosedAt?: string;\n}\n\nexport interface MaterialValidationOptions {\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** When set, every referenced problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n}\n\n/** Validates an exercode markdown material file (frontmatter, embedded questions, problem references). */\nexport async function validateMaterialFile(\n materialFilePath: string,\n options: MaterialValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const materialId = basename(materialFilePath).replace(/\\.[^/.]+$/, '');\n if (!LEARNING_MATERIAL_ID_REGEX.test(materialId)) {\n errors.push(`material ID \"${materialId}\" (file name without extension) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n\n let markdown: string;\n try {\n markdown = await readFile(materialFilePath, 'utf8');\n } catch {\n errors.push(`material file not found: ${materialFilePath}`);\n return result;\n }\n\n let attributes: unknown;\n let body: string;\n try {\n ({ attributes, body } = parseFrontmatter(markdown));\n } catch (error) {\n errors.push(`invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return result;\n }\n\n const parsedFrontmatter = materialFrontmatterSchema.safeParse(attributes);\n if (!parsedFrontmatter.success) {\n errors.push(...formatZodIssues(parsedFrontmatter.error, 'frontmatter'));\n }\n const frontmatter = parsedFrontmatter.success ? parsedFrontmatter.data : undefined;\n\n const questionsInCodeBlocks = parseQuestionCodeBlocks(body, errors);\n const questionBlockIds = questionsInCodeBlocks.map((question) => question.id);\n const questions = [...(frontmatter?.questions ?? []), ...questionsInCodeBlocks];\n for (const question of questions) {\n validateQuestion(question, errors);\n }\n reportDuplicateIds(\n questions.map((question) => question.id),\n 'question',\n errors\n );\n // Turtle-graphics links in the body merge into the frontmatter list like problem links do,\n // and exercode rejects duplicates in the merged list; deferred until after the question blocks\n // are removed below so links inside question text don't count.\n\n // Exercode's import validation runs two checks: the body must not link the same problem,\n // question or turtle-graphics question twice (every occurrence of the lax link pattern counts),\n // and the generated reference list (frontmatter entries plus the judge's deduplicated body links)\n // must not repeat an ID. The judge replaces recognized question blocks with @[question](id) links\n // before collecting links, so links that appear only inside question text must not count, while\n // each block counts as one question link next to explicit @[question](id) links.\n const bodyWithoutQuestionBlocks = body.replaceAll(QUESTION_CODE_BLOCK_REGEX, '\\n');\n reportDuplicateIds(\n [...questionBlockIds, ...collectLinkedIds(bodyWithoutQuestionBlocks, QUESTION_LINK_REGEX)],\n 'question link',\n errors\n );\n reportDuplicateIds(collectLinkedIds(bodyWithoutQuestionBlocks, PROBLEM_ASSIGNMENT_REGEX), 'problem link', errors);\n reportDuplicateIds(\n collectLinkedIds(bodyWithoutQuestionBlocks, TURTLE_GRAPHICS_ASSIGNMENT_REGEX),\n 'turtle-graphics question link',\n errors\n );\n const linkedTurtleGraphicsQuestionIds = new Set(\n collectLinkedIds(bodyWithoutQuestionBlocks, TURTLE_GRAPHICS_QUESTION_LINK_REGEX)\n );\n reportDuplicateIds(\n [\n ...(frontmatter?.turtleGraphicsQuestions ?? []).map((question) => question.id),\n ...linkedTurtleGraphicsQuestionIds,\n ],\n 'turtleGraphicsQuestion',\n errors\n );\n // Only the links the judge collects become references, and it deduplicates them before merging.\n const problemIds = [\n ...(frontmatter?.problems ?? []).map((problem) => problem.id),\n ...new Set(collectLinkedIds(bodyWithoutQuestionBlocks, PROBLEM_LINK_REGEX)),\n ];\n reportDuplicateIds(problemIds, 'problem', errors);\n reportForeignProblemCourseIds(frontmatter?.problems ?? [], options.courseId, errors);\n const availableProblemIds =\n options.availableProblemIds ??\n (options.problemsDirectoryPath === undefined\n ? undefined\n : await collectProblemIdsFromDirectory(options.problemsDirectoryPath, errors));\n if (availableProblemIds !== undefined) {\n reportDanglingProblemReferences(problemIds, availableProblemIds, options.problemsDirectoryPath, errors);\n }\n\n reportExcessChatMarkers(body, errors);\n\n if (frontmatter) {\n validateSubmissionPeriods(\n mergeSubmissionPeriods(frontmatter, options.courseSubmissionPeriods),\n errors,\n frontmatter\n );\n }\n return result;\n}\n\nfunction collectLinkedIds(body: string, linkRegex: RegExp): string[] {\n return [...body.matchAll(linkRegex)].map((match) => match[1]).filter((id) => id !== undefined);\n}\n\n/** Applies the judge's course→material config merge (`material[field] ?? course[field]`). */\nexport function mergeSubmissionPeriods(\n own: SubmissionPeriods,\n courseDefaults: SubmissionPeriods | undefined\n): SubmissionPeriods {\n return {\n submissionOpenedAt: own.submissionOpenedAt ?? courseDefaults?.submissionOpenedAt,\n submissionSoftClosedAt: own.submissionSoftClosedAt ?? courseDefaults?.submissionSoftClosedAt,\n submissionHardClosedAt: own.submissionHardClosedAt ?? courseDefaults?.submissionHardClosedAt,\n };\n}\n\n/** exercode rejects more than one standalone chat delimiter line in material and problem bodies. */\nexport function reportExcessChatMarkers(body: string, errors: string[]): void {\n const chatMarkerCount = [...body.matchAll(STANDALONE_CHAT_MARKER_REGEX)].length;\n if (chatMarkerCount > 1) {\n errors.push(`at most one \\`${CHAT_MARKER}\\` marker is allowed, but found ${chatMarkerCount}`);\n }\n}\n\n/** The judge's resolveProblemCourseId throws when a problem reference names a different course. */\nexport function reportForeignProblemCourseIds(\n problems: readonly { id: string; courseId?: string }[],\n courseId: string | undefined,\n errors: string[]\n): void {\n if (courseId === undefined) return;\n for (const problem of problems) {\n if (problem.courseId !== undefined && problem.courseId !== courseId) {\n errors.push(`problem \"${problem.id}\": courseId must be \"${courseId}\" but is \"${problem.courseId}\"`);\n }\n }\n}\n\nfunction parseQuestionCodeBlocks(body: string, errors: string[]): MaterialQuestion[] {\n const questions: MaterialQuestion[] = [];\n const matches = [...body.matchAll(QUESTION_CODE_BLOCK_REGEX)];\n // The judge silently renders unparsed question blocks as plain code blocks, so a block the\n // strict regex misses (CRLF line endings, or no newline after the closing fence) would break\n // in production while looking fine here; flag it instead of validating a lie. Openers inside a\n // recognized block (e.g. an example fence in a question's text) are consumed with that block.\n const leftoverOpenerCount = [\n ...body.replaceAll(QUESTION_CODE_BLOCK_REGEX, '\\n').matchAll(QUESTION_CODE_BLOCK_OPENER_REGEX),\n ].length;\n if (leftoverOpenerCount > 0) {\n errors.push(\n `${leftoverOpenerCount} question code block(s) will not be recognized by the judge parser; separate consecutive question blocks with a blank line, use LF line endings, no indentation or trailing whitespace on the fence line, and end the closing fence with a newline`\n );\n }\n for (const [blockIndex, match] of matches.entries()) {\n const yamlText = match.groups?.['yaml'] ?? '';\n let rawQuestion: unknown;\n try {\n rawQuestion = parseYaml(yamlText);\n } catch (error) {\n errors.push(`question block ${blockIndex + 1}: invalid YAML: ${getErrorMessage(error)}`);\n continue;\n }\n const parsed = questionInCodeBlockSchema.safeParse(rawQuestion);\n if (parsed.success) {\n questions.push(parsed.data);\n } else {\n errors.push(...formatZodIssues(parsed.error, `question block ${blockIndex + 1}`));\n }\n }\n return questions;\n}\n\nfunction validateQuestion(question: MaterialQuestion, errors: string[]): void {\n const isSurvey = question.isSurvey === true;\n const [answerFieldName, isAnswerDefined] =\n question.type === 'select'\n ? (['answerIndex', question.answerIndex !== undefined] as const)\n : question.type === 'select_multiple'\n ? (['answerIndices', question.answerIndices !== undefined] as const)\n : (['answerPattern', question.answerPattern !== undefined] as const);\n if (isSurvey && isAnswerDefined) {\n errors.push(`question ${question.id}: \\`${answerFieldName}\\` must not be set when \\`isSurvey\\` is true`);\n }\n if (!isSurvey && !isAnswerDefined) {\n errors.push(`question ${question.id}: \\`${answerFieldName}\\` is required when \\`isSurvey\\` is not true`);\n }\n\n if (question.type === 'select' || question.type === 'select_multiple') {\n const answerIndices =\n question.type === 'select'\n ? Array.isArray(question.answerIndex)\n ? question.answerIndex\n : question.answerIndex === undefined\n ? []\n : [question.answerIndex]\n : (question.answerIndices ?? []);\n // An empty answer set is broken at runtime (select: never correct; select_multiple: an empty\n // submission grades as correct), and exercode's material editor rejects it as well.\n if (!isSurvey && isAnswerDefined && answerIndices.length === 0) {\n errors.push(`question ${question.id}: \\`${answerFieldName}\\` must contain at least one correct answer`);\n }\n for (const answerIndex of answerIndices) {\n if (!Number.isInteger(answerIndex) || answerIndex < 0 || answerIndex >= question.options.length) {\n errors.push(\n `question ${question.id}: ${answerFieldName} out of bounds: ${answerIndex}; expected 0 <= index < options length ${question.options.length}`\n );\n }\n }\n return;\n }\n\n if (isSurvey || question.answerPattern === undefined) return;\n const answerPattern = question.answerPattern.trim();\n let answerRegExp: RegExp;\n let rawAnswerRegExp: RegExp;\n try {\n answerRegExp = new RegExp(`^(?:${answerPattern})$`);\n // Compiled inside the same guard: a pattern like `a)(b` is valid only once wrapped above,\n // so an unguarded raw compile would crash the CLI instead of reporting an error.\n rawAnswerRegExp = new RegExp(question.answerPattern);\n } catch {\n errors.push(\n `question ${question.id}: \\`answerPattern\\` is not a valid regular expression: ${question.answerPattern}`\n );\n return;\n }\n // Mirrors the judge heuristic: a pattern that doesn't match itself likely uses RegExp syntax,\n // so a modelAnswer is required to prove that some concrete answer is accepted.\n // Like the judge, an empty modelAnswer counts as missing.\n // The judge runs this heuristic on the untrimmed pattern and trims only for the anchored match.\n if (!question.modelAnswer && !rawAnswerRegExp.test(question.answerPattern)) {\n errors.push(`question ${question.id}: \\`answerPattern\\` seems a RegExp but \\`modelAnswer\\` is missing`);\n }\n if (question.modelAnswer && !answerRegExp.test(question.modelAnswer.trim())) {\n errors.push(`question ${question.id}: \\`modelAnswer\\` doesn't match \\`answerPattern\\``);\n }\n}\n\n/**\n * Reports inverted submission periods; shared by material, course.yaml, and contest validation.\n * When `ownPeriods` is given (the merged-values case), pairs whose both values are inherited from\n * the course are skipped: they are the course's mistake and are already reported against\n * course.yaml, so repeating them per material would send agents to files with nothing to fix.\n */\nexport function validateSubmissionPeriods(\n periods: SubmissionPeriods,\n errors: string[],\n ownPeriods: SubmissionPeriods = periods\n): void {\n const pairs = [\n ['submissionOpenedAt', 'submissionSoftClosedAt'],\n ['submissionSoftClosedAt', 'submissionHardClosedAt'],\n ['submissionOpenedAt', 'submissionHardClosedAt'],\n ] as const satisfies readonly (readonly [keyof SubmissionPeriods, keyof SubmissionPeriods])[];\n for (const [earlierName, laterName] of pairs) {\n const earlierValue = periods[earlierName];\n const laterValue = periods[laterName];\n if (earlierValue === undefined || laterValue === undefined) continue;\n if (ownPeriods[earlierName] === undefined && ownPeriods[laterName] === undefined) continue;\n if (new Date(laterValue).getTime() < new Date(earlierValue).getTime()) {\n errors.push(`${earlierName} (${earlierValue}) must be <= ${laterName} (${laterValue})`);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validateMaterial.js","names":["parseYaml","QUESTION_CODE_BLOCK_REGEX","QUESTION_CODE_BLOCK_OPENER_REGEX","PROBLEM_LINK_REGEX","TURTLE_GRAPHICS_QUESTION_LINK_REGEX","PROBLEM_ASSIGNMENT_REGEX","QUESTION_LINK_REGEX","TURTLE_GRAPHICS_ASSIGNMENT_REGEX","STANDALONE_CHAT_MARKER_REGEX","validateMaterialFile","errors","result","materialId","basename","materialFilePath","LEARNING_MATERIAL_ID_REGEX","markdown","readFile","attributes","body","parseFrontmatter","error","getErrorMessage","parsedFrontmatter","materialFrontmatterSchema","formatZodIssues","frontmatter","questionsInCodeBlocks","parseQuestionCodeBlocks","questionBlockIds","question","questions","validateQuestion","reportDuplicateIds","bodyWithoutQuestionBlocks","collectLinkedIds","linkedTurtleGraphicsQuestionIds","problemIds","problem","reportForeignProblemCourseIds","options","availableProblemIds","collectProblemIdsFromDirectory","reportDanglingProblemReferences","reportExcessChatMarkers","validateSubmissionPeriods","mergeSubmissionPeriods","linkRegex","match","id","own","courseDefaults","chatMarkerCount","courseId","problems","matches","leftoverOpenerCount","blockIndex","yamlText","\"yaml\"","rawQuestion","parse","parsed","questionInCodeBlockSchema","isSurvey","answerFieldName","isAnswerDefined","answerIndices","answerIndex","answerPattern","answerRegExp","rawAnswerRegExp","periods","earlierName","laterName","earlierValue","laterValue","ownPeriods"],"sources":["../../src/learningMaterial/validateMaterial.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\nimport { parseFrontmatter } from './frontmatter.js';\nimport { collectProblemIdsFromDirectory, reportDanglingProblemReferences } from './fsHelpers.js';\nimport {\n LEARNING_MATERIAL_ID_REGEX,\n materialFrontmatterSchema,\n type MaterialQuestion,\n questionInCodeBlockSchema,\n} from './schemas.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\n// Same patterns the judge uses to extract embedded questions and problem links from material bodies.\n// The block regex intentionally requires LF line endings and a trailing newline after the closing\n// fence, exactly like the judge parser; blocks it misses are reported by detecting leftover openers.\nconst QUESTION_CODE_BLOCK_REGEX = /(?:^|\\n)(?<fence>`{3,}|~{3,})ya?ml +question\\n(?<yaml>[\\s\\S]+?)\\n\\k<fence>\\n/g;\n// Strictly laxer than the parser regex (tolerates CRLF, indentation, and stray whitespace) so\n// question-looking blocks the judge parser would silently skip surface as leftover openers.\nconst QUESTION_CODE_BLOCK_OPENER_REGEX = /^[ \\t]*(?:`{3,}|~{3,})[ \\t]*ya?ml[ \\t]+question[ \\t]*\\r?$/gm;\nconst PROBLEM_LINK_REGEX = /\\[.*?]\\(problems\\/([0-9_a-z-]+?)\\)/g;\nconst TURTLE_GRAPHICS_QUESTION_LINK_REGEX = /\\[.*?]\\(turtle-graphics-questions\\/([0-9_a-z-]+?)\\)/g;\n// Exercode's duplicate-assignment check scans the body with these patterns, whose link text may\n// span lines (unlike the judge's reference collection above).\nconst PROBLEM_ASSIGNMENT_REGEX = /\\[[^\\]]*]\\(problems\\/([0-9_a-z-]+)\\)/g;\nconst QUESTION_LINK_REGEX = /@\\[question]\\(([0-9_a-z-]+)\\)/g;\nconst TURTLE_GRAPHICS_ASSIGNMENT_REGEX = /\\[[^\\]]*]\\(turtle-graphics-questions\\/([0-9_a-z-]+)\\)/g;\nconst CHAT_MARKER = '<!-- chat -->';\n// exercode only counts the marker when it stands alone on a line, so fenced or inline mentions don't count.\nconst STANDALONE_CHAT_MARKER_REGEX = /(^|\\r?\\n)<!-- chat -->[ \\t]*(?=\\r?\\n|$)/g;\n\nexport interface SubmissionPeriods {\n submissionOpenedAt?: string;\n submissionSoftClosedAt?: string;\n submissionHardClosedAt?: string;\n}\n\nexport interface MaterialValidationOptions {\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** When set, every referenced problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n}\n\n/** Validates an exercode markdown material file (frontmatter, embedded questions, problem references). */\nexport async function validateMaterialFile(\n materialFilePath: string,\n options: MaterialValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const materialId = basename(materialFilePath).replace(/\\.[^/.]+$/, '');\n if (!LEARNING_MATERIAL_ID_REGEX.test(materialId)) {\n errors.push(`material ID \"${materialId}\" (file name without extension) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n\n let markdown: string;\n try {\n markdown = await readFile(materialFilePath, 'utf8');\n } catch {\n errors.push(`material file not found: ${materialFilePath}`);\n return result;\n }\n\n let attributes: unknown;\n let body: string;\n try {\n ({ attributes, body } = parseFrontmatter(markdown));\n } catch (error) {\n errors.push(`invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return result;\n }\n\n const parsedFrontmatter = materialFrontmatterSchema.safeParse(attributes);\n if (!parsedFrontmatter.success) {\n errors.push(...formatZodIssues(parsedFrontmatter.error, 'frontmatter'));\n }\n const frontmatter = parsedFrontmatter.success ? parsedFrontmatter.data : undefined;\n\n const questionsInCodeBlocks = parseQuestionCodeBlocks(body, errors);\n const questionBlockIds = questionsInCodeBlocks.map((question) => question.id);\n const questions = [...(frontmatter?.questions ?? []), ...questionsInCodeBlocks];\n for (const question of questions) {\n validateQuestion(question, errors);\n }\n reportDuplicateIds(\n questions.map((question) => question.id),\n 'question',\n errors\n );\n // Turtle-graphics links in the body merge into the frontmatter list like problem links do,\n // and exercode rejects duplicates in the merged list; deferred until after the question blocks\n // are removed below so links inside question text don't count.\n\n // Exercode's import validation runs two checks: the body must not link the same problem,\n // question or turtle-graphics question twice (every occurrence of the lax link pattern counts),\n // and the generated reference list (frontmatter entries plus the judge's deduplicated body links)\n // must not repeat an ID. The judge replaces recognized question blocks with @[question](id) links\n // before collecting links, so links that appear only inside question text must not count, while\n // each block counts as one question link next to explicit @[question](id) links.\n const bodyWithoutQuestionBlocks = body.replaceAll(QUESTION_CODE_BLOCK_REGEX, '\\n');\n reportDuplicateIds(\n [...questionBlockIds, ...collectLinkedIds(bodyWithoutQuestionBlocks, QUESTION_LINK_REGEX)],\n 'question link',\n errors\n );\n reportDuplicateIds(collectLinkedIds(bodyWithoutQuestionBlocks, PROBLEM_ASSIGNMENT_REGEX), 'problem link', errors);\n reportDuplicateIds(\n collectLinkedIds(bodyWithoutQuestionBlocks, TURTLE_GRAPHICS_ASSIGNMENT_REGEX),\n 'turtle-graphics question link',\n errors\n );\n const linkedTurtleGraphicsQuestionIds = new Set(\n collectLinkedIds(bodyWithoutQuestionBlocks, TURTLE_GRAPHICS_QUESTION_LINK_REGEX)\n );\n reportDuplicateIds(\n [\n ...(frontmatter?.turtleGraphicsQuestions ?? []).map((question) => question.id),\n ...linkedTurtleGraphicsQuestionIds,\n ],\n 'turtleGraphicsQuestion',\n errors\n );\n // Only the links the judge collects become references, and it deduplicates them before merging.\n const problemIds = [\n ...(frontmatter?.problems ?? []).map((problem) => problem.id),\n ...new Set(collectLinkedIds(bodyWithoutQuestionBlocks, PROBLEM_LINK_REGEX)),\n ];\n reportDuplicateIds(problemIds, 'problem', errors);\n reportForeignProblemCourseIds(frontmatter?.problems ?? [], options.courseId, errors);\n const availableProblemIds =\n options.availableProblemIds ??\n (options.problemsDirectoryPath === undefined\n ? undefined\n : await collectProblemIdsFromDirectory(options.problemsDirectoryPath, errors));\n if (availableProblemIds !== undefined) {\n reportDanglingProblemReferences(problemIds, availableProblemIds, options.problemsDirectoryPath, errors);\n }\n\n reportExcessChatMarkers(body, errors);\n\n if (frontmatter) {\n validateSubmissionPeriods(\n mergeSubmissionPeriods(frontmatter, options.courseSubmissionPeriods),\n errors,\n frontmatter\n );\n }\n return result;\n}\n\nfunction collectLinkedIds(body: string, linkRegex: RegExp): string[] {\n return [...body.matchAll(linkRegex)].map((match) => match[1]).filter((id) => id !== undefined);\n}\n\n/** Applies the judge's course→material config merge (`material[field] ?? course[field]`). */\nexport function mergeSubmissionPeriods(\n own: SubmissionPeriods,\n courseDefaults: SubmissionPeriods | undefined\n): SubmissionPeriods {\n return {\n submissionOpenedAt: own.submissionOpenedAt ?? courseDefaults?.submissionOpenedAt,\n submissionSoftClosedAt: own.submissionSoftClosedAt ?? courseDefaults?.submissionSoftClosedAt,\n submissionHardClosedAt: own.submissionHardClosedAt ?? courseDefaults?.submissionHardClosedAt,\n };\n}\n\n/** exercode rejects more than one standalone chat delimiter line in material and problem bodies. */\nexport function reportExcessChatMarkers(body: string, errors: string[]): void {\n const chatMarkerCount = [...body.matchAll(STANDALONE_CHAT_MARKER_REGEX)].length;\n if (chatMarkerCount > 1) {\n errors.push(`at most one \\`${CHAT_MARKER}\\` marker is allowed, but found ${chatMarkerCount}`);\n }\n}\n\n/** The judge's resolveProblemCourseId throws when a problem reference names a different course. */\nexport function reportForeignProblemCourseIds(\n problems: readonly { id: string; courseId?: string }[],\n courseId: string | undefined,\n errors: string[]\n): void {\n if (courseId === undefined) return;\n for (const problem of problems) {\n if (problem.courseId !== undefined && problem.courseId !== courseId) {\n errors.push(`problem \"${problem.id}\": courseId must be \"${courseId}\" but is \"${problem.courseId}\"`);\n }\n }\n}\n\nfunction parseQuestionCodeBlocks(body: string, errors: string[]): MaterialQuestion[] {\n const questions: MaterialQuestion[] = [];\n const matches = [...body.matchAll(QUESTION_CODE_BLOCK_REGEX)];\n // The judge silently renders unparsed question blocks as plain code blocks, so a block the\n // strict regex misses (CRLF line endings, or no newline after the closing fence) would break\n // in production while looking fine here; flag it instead of validating a lie. Openers inside a\n // recognized block (e.g. an example fence in a question's text) are consumed with that block.\n const leftoverOpenerCount = [\n ...body.replaceAll(QUESTION_CODE_BLOCK_REGEX, '\\n').matchAll(QUESTION_CODE_BLOCK_OPENER_REGEX),\n ].length;\n if (leftoverOpenerCount > 0) {\n errors.push(\n `${leftoverOpenerCount} question code block(s) will not be recognized by the judge parser; separate consecutive question blocks with a blank line, use LF line endings, no indentation or trailing whitespace on the fence line, and end the closing fence with a newline`\n );\n }\n for (const [blockIndex, match] of matches.entries()) {\n const yamlText = match.groups?.['yaml'] ?? '';\n let rawQuestion: unknown;\n try {\n rawQuestion = parseYaml(yamlText);\n } catch (error) {\n errors.push(`question block ${blockIndex + 1}: invalid YAML: ${getErrorMessage(error)}`);\n continue;\n }\n const parsed = questionInCodeBlockSchema.safeParse(rawQuestion);\n if (parsed.success) {\n questions.push(parsed.data);\n } else {\n errors.push(...formatZodIssues(parsed.error, `question block ${blockIndex + 1}`));\n }\n }\n return questions;\n}\n\nfunction validateQuestion(question: MaterialQuestion, errors: string[]): void {\n const isSurvey = question.isSurvey === true;\n const [answerFieldName, isAnswerDefined] =\n question.type === 'select'\n ? (['answerIndex', question.answerIndex !== undefined] as const)\n : question.type === 'select_multiple'\n ? (['answerIndices', question.answerIndices !== undefined] as const)\n : (['answerPattern', question.answerPattern !== undefined] as const);\n if (isSurvey && isAnswerDefined) {\n errors.push(`question ${question.id}: \\`${answerFieldName}\\` must not be set when \\`isSurvey\\` is true`);\n }\n if (!isSurvey && !isAnswerDefined) {\n errors.push(`question ${question.id}: \\`${answerFieldName}\\` is required when \\`isSurvey\\` is not true`);\n }\n\n if (question.type === 'select' || question.type === 'select_multiple') {\n const answerIndices =\n question.type === 'select'\n ? Array.isArray(question.answerIndex)\n ? question.answerIndex\n : question.answerIndex === undefined\n ? []\n : [question.answerIndex]\n : (question.answerIndices ?? []);\n // An empty answer set is broken at runtime (select: never correct; select_multiple: an empty\n // submission grades as correct), and exercode's material editor rejects it as well.\n if (!isSurvey && isAnswerDefined && answerIndices.length === 0) {\n errors.push(`question ${question.id}: \\`${answerFieldName}\\` must contain at least one correct answer`);\n }\n for (const answerIndex of answerIndices) {\n if (!Number.isInteger(answerIndex) || answerIndex < 0 || answerIndex >= question.options.length) {\n errors.push(\n `question ${question.id}: ${answerFieldName} out of bounds: ${answerIndex}; expected 0 <= index < options length ${question.options.length}`\n );\n }\n }\n return;\n }\n\n if (isSurvey || question.answerPattern === undefined) return;\n const answerPattern = question.answerPattern.trim();\n let answerRegExp: RegExp;\n let rawAnswerRegExp: RegExp;\n try {\n answerRegExp = new RegExp(`^(?:${answerPattern})$`);\n // Compiled inside the same guard: a pattern like `a)(b` is valid only once wrapped above,\n // so an unguarded raw compile would crash the CLI instead of reporting an error.\n rawAnswerRegExp = new RegExp(question.answerPattern);\n } catch {\n errors.push(\n `question ${question.id}: \\`answerPattern\\` is not a valid regular expression: ${question.answerPattern}`\n );\n return;\n }\n // Mirrors the judge heuristic: a pattern that doesn't match itself likely uses RegExp syntax,\n // so a modelAnswer is required to prove that some concrete answer is accepted.\n // Like the judge, an empty modelAnswer counts as missing.\n // The judge runs this heuristic on the untrimmed pattern and trims only for the anchored match.\n if (!question.modelAnswer && !rawAnswerRegExp.test(question.answerPattern)) {\n errors.push(`question ${question.id}: \\`answerPattern\\` seems a RegExp but \\`modelAnswer\\` is missing`);\n }\n if (question.modelAnswer && !answerRegExp.test(question.modelAnswer.trim())) {\n errors.push(`question ${question.id}: \\`modelAnswer\\` doesn't match \\`answerPattern\\``);\n }\n}\n\n/**\n * Reports inverted submission periods; shared by material, course.yaml, and contest validation.\n * When `ownPeriods` is given (the merged-values case), pairs whose both values are inherited from\n * the course are skipped: they are the course's mistake and are already reported against\n * course.yaml, so repeating them per material would send agents to files with nothing to fix.\n */\nexport function validateSubmissionPeriods(\n periods: SubmissionPeriods,\n errors: string[],\n ownPeriods: SubmissionPeriods = periods\n): void {\n const pairs = [\n ['submissionOpenedAt', 'submissionSoftClosedAt'],\n ['submissionSoftClosedAt', 'submissionHardClosedAt'],\n ['submissionOpenedAt', 'submissionHardClosedAt'],\n ] as const satisfies readonly (readonly [keyof SubmissionPeriods, keyof SubmissionPeriods])[];\n for (const [earlierName, laterName] of pairs) {\n const earlierValue = periods[earlierName];\n const laterValue = periods[laterName];\n if (earlierValue === undefined || laterValue === undefined) continue;\n if (ownPeriods[earlierName] === undefined && ownPeriods[laterName] === undefined) continue;\n if (new Date(laterValue).getTime() < new Date(earlierValue).getTime()) {\n errors.push(`${earlierName} (${earlierValue}) must be <= ${laterName} (${laterValue})`);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateProblem.cjs","names":["resolve","isRegularDirectory","isDirectory","basename","LEARNING_MATERIAL_ID_REGEX","relative","readdir","join","isRegularFile","isFile","judgesTestCasesWithoutExpectations","findLanguageDefinitionByPath","findDefaultStdioHarnessFiles","parseFrontmatter","readFile","getErrorMessage","problemFrontmatterSchema","formatZodIssues","stat","MAX_COMPARED_FILE_BYTES","MAX_STDOUT_LENGTH","EXAMPLE_TEST_CASE_ID_PATTERN","availableLanguageIds","readSourceFilesRecursively","normalizeCodeRule","removeCommentsInSourceCode","require_compareExpectedOutputFiles","require_defaultStdioHarness","require_removeCommentsInSourceCode","require_problem","require_schemas","require_fsHelpers","require_frontmatter","require_validationResult","require_validateMaterial","require_findLanguageDefinitionByPath","require_readProblemMarkdownFrontMatter","require_stdioJudgeRules","node_path","node_fs_promises","_willbooster_shared_lib","ALLOWED_PARSING_HELPERS","PARSING_CODE_SNIPPETS","SHARED_FILE_INPUT_NAME","IGNORED_TEST_CASE_ENTRY_NAMES","validateProblemDirectory","errors","warnings","result","absoluteDirectoryPath","problemDirectoryPath","problemId","linkPath","listSymbolicLinks","problemDirents","dirent","d1","d2","problemFilePath","frontmatter","parseProblemFrontmatter","harness","readHarnessFiles","requiresExpectedOutput","validateTestCasePresence","readFileTestCases","isScoredAutomatically","validateHarnessFiles","modelAnswers","readModelAnswers","usableModelAnswers","isRunnable","answer","file","validateCodePatterns","validateRequiredSubmissionFiles","validateTemplates","hasJudgeTs","defaultHarnessFileNames","hasParsedFrontmatter","attributes","body","error","parsed","testCasesDirectoryPath","dirents","idToTestCase","getTestCase","testCase","testCaseId","listFilesRecursively","filePaths","filePath","hasCustomJudgeTs","fileNames","f1","f2","fileName","content","testCases","t1","t2","hasAnyExpectedOutput","hasExpectedOutput","maxStdoutLength","stdinToFirstId","firstId","linkPaths","directoryPath","entryPath","p1","p2","fileTestCases","reportTestCaseCompositionIssues","modelAnswersDirectoryPath","files","requiredRegExps","compilePatterns","forbiddenRegExps","forbiddenTexts","rule","pattern","regExp","snippet","text","modelAnswer","strippedFiles","readSourceCodeWithoutComments","matched","forbiddenText","filePathSet","requiredPath","languageDefinition","grammar","compiled","rules","fieldName","templatesDirectoryPath","entries","hasFiles","hasDirectories","directoryNames","templateDirectoryNames","templateDirectoryName","templateFiles","templateContentByPath","judgedFiles"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { MAX_COMPARED_FILE_BYTES } from '../helpers/compareExpectedOutputFiles.js';\nimport { findDefaultStdioHarnessFiles, type HarnessFileName } from '../helpers/defaultStdioHarness.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { judgesTestCasesWithoutExpectations } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { removeCommentsInSourceCode } from '../helpers/removeCommentsInSourceCode.js';\nimport { EXAMPLE_TEST_CASE_ID_PATTERN, MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { type CodeRule, normalizeCodeRule } from '../types/problem.js';\n\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n readSourceFilesRecursively,\n type SourceFile,\n} from './fsHelpers.js';\nimport {\n availableLanguageIds,\n LEARNING_MATERIAL_ID_REGEX,\n problemFrontmatterSchema,\n type ProblemFrontmatter,\n} from './schemas.js';\nimport { reportExcessChatMarkers } from './validateMaterial.js';\nimport { formatZodIssues, type ValidationResult } from './validationResult.js';\n\nconst RECOMMENDED_MIN_TEST_CASE_COUNT = 4;\n// Forbidden patterns must not restrict classes/functions commonly needed just to parse stdin (from gen-em).\n// Instead of a substring check on the pattern source (\"print\" contains \"int\"), each forbidden\n// pattern is applied to representative parsing code; a hit means the pattern would forbid parsing.\nconst ALLOWED_PARSING_HELPERS = ['Scanner', 'map', 'int', 'split'] as const;\nconst PARSING_CODE_SNIPPETS = [\n 'a, b = map(int, input().split())',\n 'Scanner scanner = new Scanner(System.in);',\n \"const [a, b] = line.split(' ').map(Number);\",\n] as const;\n\ninterface FileTestCase {\n id: string;\n stdin?: string;\n stdout?: string;\n /** Whether `<id>.fin/` (input files copied into the working directory) exists and holds at least one file. */\n hasFileInput: boolean;\n /** Whether `<id>.fout/` (expected output files) exists and holds at least one file. */\n hasFileOutput: boolean;\n /** Whether `<id>.json` (configuration read by a custom `judge.ts`) exists. */\n hasJudgeConfig: boolean;\n}\n\n// The importer rejects a longer expected stdout whichever judge the problem has.\nconst IMPORTER_MAX_STDOUT_LENGTH = 100_000;\n// `_shared.fin/` holds files copied for every test case; it is not a test case itself.\nconst SHARED_FILE_INPUT_NAME = '_shared';\n// The importer leaves generated artifacts out of the packaged problem, so a directory holding only\n// these is as good as empty. Any other file counts, even a `.gitkeep` placeholder.\nconst IGNORED_TEST_CASE_ENTRY_NAMES: ReadonlySet<string> = new Set(['.DS_Store', '__pycache__']);\n\ninterface ModelAnswer {\n id: string;\n files: SourceFile[];\n}\n\n/**\n * Validates a judge v2 problem directory (problem.md, test_cases, model_answers, templates, and —\n * for custom judging or custom debugging only — judge.ts/debug.ts).\n */\nexport async function validateProblemDirectory(problemDirectoryPath: string): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const absoluteDirectoryPath = resolve(problemDirectoryPath);\n // Problem discovery does not traverse a symbolic link, so a linked problem directory is never imported.\n if (!(await isRegularDirectory(absoluteDirectoryPath))) {\n errors.push(\n (await isDirectory(absoluteDirectoryPath))\n ? `problem directory is a symbolic link, which the judge does not traverse: ${problemDirectoryPath}`\n : `problem directory not found: ${problemDirectoryPath}`\n );\n return result;\n }\n\n const problemId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(`problem ID \"${problemId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n // The judge's front-matter reader takes the first `problem.md` or `*.problem.md` it finds, so a\n // leftover v1 file can be judged instead of problem.md.\n // The judge readers enumerate regular files and directories only, so a symbolic link anywhere in\n // the problem is ignored at import time and would silently drop or change what is judged.\n for (const linkPath of await listSymbolicLinks(absoluteDirectoryPath)) {\n errors.push(\n `${relative(absoluteDirectoryPath, linkPath)} is a symbolic link, which the judge ignores; commit a real file or directory`\n );\n }\n const problemDirents = await readdir(absoluteDirectoryPath, { withFileTypes: true });\n for (const dirent of problemDirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (dirent.isFile() && dirent.name.endsWith('.problem.md')) {\n errors.push(`found v1 problem file ${dirent.name}; use the v2 layout with a file named exactly problem.md`);\n }\n }\n\n const problemFilePath = join(absoluteDirectoryPath, 'problem.md');\n // The judge discovers problems by their problem.md and skips symbolic links (reported above).\n if (!(await isRegularFile(problemFilePath))) {\n if (!(await isFile(problemFilePath))) {\n errors.push('problem.md not found; a v2 problem directory must contain problem.md');\n }\n return result;\n }\n\n const frontmatter = await parseProblemFrontmatter(problemFilePath, errors);\n const harness = await readHarnessFiles(absoluteDirectoryPath);\n const requiresExpectedOutput = frontmatter === undefined || !judgesTestCasesWithoutExpectations(frontmatter);\n const fileTestCases = await readFileTestCases(\n absoluteDirectoryPath,\n { hasCustomJudgeTs: harness.hasCustomJudgeTs, requiresExpectedOutput },\n errors,\n warnings\n );\n // A default judge.ts is about to be deleted, so treat only a genuinely custom judge.ts as a\n // special judge; otherwise the case-less error would surface only after the deletion.\n validateTestCasePresence(frontmatter, fileTestCases, harness.hasCustomJudgeTs, errors, warnings);\n\n // A problem whose frontmatter failed to parse already has a primary error; deriving secondary\n // judge.ts/model-answer errors from it would send the fix loop after the wrong problems.\n const isScoredAutomatically =\n frontmatter !== undefined && frontmatter.isManualScoringRequired !== true && frontmatter.type !== 'prompt_study';\n validateHarnessFiles(harness, frontmatter !== undefined, errors, warnings);\n\n const modelAnswers = await readModelAnswers(absoluteDirectoryPath, errors, warnings);\n // Every model answer directory is judged by the all-problem check, so each one must hold what\n // its judge runs: a custom judge defines that itself (e.g. a CSV submission), while the stdio\n // judge needs a non-empty file of a supported language as its entry point.\n const usableModelAnswers = modelAnswers.filter((answer) => {\n // A custom judge may accept an empty data file, so only the stdio judge needs content.\n if (harness.hasCustomJudgeTs) return true;\n const isRunnable = answer.files.some(\n (file) => findLanguageDefinitionByPath(file.path) !== undefined && file.data.trim().length > 0\n );\n if (!isRunnable) {\n errors.push(\n `model answer directory \"${answer.id}\" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`\n );\n }\n return isRunnable;\n });\n if (isScoredAutomatically && modelAnswers.length === 0) {\n errors.push('no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files');\n }\n if (frontmatter) {\n validateCodePatterns(frontmatter, usableModelAnswers, errors, warnings);\n validateRequiredSubmissionFiles(frontmatter, usableModelAnswers, errors);\n }\n await validateTemplates(absoluteDirectoryPath, usableModelAnswers, harness.hasCustomJudgeTs, errors, warnings);\n\n return result;\n}\n\ninterface HarnessFiles {\n hasDebugTs: boolean;\n rejectedDefaultHarnessFileNames: readonly HarnessFileName[];\n hasCustomJudgeTs: boolean;\n}\n\nasync function readHarnessFiles(absoluteDirectoryPath: string): Promise<HarnessFiles> {\n const hasJudgeTs = await isFile(join(absoluteDirectoryPath, 'judge.ts'));\n // The same policy the `judge` subcommand and the all-problem check apply: a default judge.ts is\n // always rejected, and a default debug.ts is rejected unless it accompanies a custom judge.ts.\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(absoluteDirectoryPath);\n return {\n hasDebugTs: await isFile(join(absoluteDirectoryPath, 'debug.ts')),\n rejectedDefaultHarnessFileNames: defaultHarnessFileNames,\n hasCustomJudgeTs: hasJudgeTs && !defaultHarnessFileNames.includes('judge.ts'),\n };\n}\n\n/**\n * The judge treats a problem without judge.ts as a standard stdin/stdout problem and auto-generates\n * both the stdio judge and the stdio debug runner. Committed copies of those default harnesses are\n * rejected: they add nothing and can drift from the server's defaults, and a default judge.ts also\n * hides the standard-problem marker that enables the default debug runner.\n */\nfunction validateHarnessFiles(\n harness: HarnessFiles,\n hasParsedFrontmatter: boolean,\n errors: string[],\n warnings: string[]\n): void {\n if (harness.rejectedDefaultHarnessFileNames.includes('judge.ts')) {\n errors.push(\n 'judge.ts only calls stdioJudgePreset; delete it — the judge auto-generates it, and only a problem without judge.ts is treated as a standard stdio problem (which enables the default debug runner)'\n );\n }\n if (harness.rejectedDefaultHarnessFileNames.includes('debug.ts')) {\n errors.push(\n 'debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts'\n );\n }\n // A custom debug.ts without judge.ts stays accepted: the judge runs debug.ts whenever it is\n // present (judge.ts only selects the judging side), so it is a supported way to customize\n // debugging for a problem judged by the default stdio judge.\n // The debug feature itself is independent of the scoring mode, so warn for manual-scoring and\n // prompt_study problems too.\n if (hasParsedFrontmatter && harness.hasCustomJudgeTs && !harness.hasDebugTs) {\n warnings.push(\n \"custom judge.ts without debug.ts; the debug feature is unavailable for this problem — add debug.ts (e.g. calling stdioDebugPreset from '@exercode/problem-utils/presets/stdio') or a custom debug harness\"\n );\n }\n}\n\nasync function parseProblemFrontmatter(\n problemFilePath: string,\n errors: string[]\n): Promise<ProblemFrontmatter | undefined> {\n let attributes: unknown;\n try {\n let body: string;\n ({ attributes, body } = parseFrontmatter(await readFile(problemFilePath, 'utf8')));\n // exercode rejects problem bodies with more than one standalone chat delimiter, like materials.\n reportExcessChatMarkers(body, errors);\n } catch (error) {\n errors.push(`problem.md: invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return undefined;\n }\n const parsed = problemFrontmatterSchema.safeParse(attributes);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'problem.md frontmatter'));\n return undefined;\n }\n return parsed.data;\n}\n\nasync function readFileTestCases(\n problemDirectoryPath: string,\n { hasCustomJudgeTs, requiresExpectedOutput }: { hasCustomJudgeTs: boolean; requiresExpectedOutput: boolean },\n errors: string[],\n warnings: string[]\n): Promise<FileTestCase[]> {\n const testCasesDirectoryPath = join(problemDirectoryPath, 'test_cases');\n if (!(await isDirectory(testCasesDirectoryPath))) return [];\n\n const dirents = await readdir(testCasesDirectoryPath, { withFileTypes: true });\n const idToTestCase = new Map<string, FileTestCase>();\n const getTestCase = (testCaseId: string): FileTestCase => {\n let testCase = idToTestCase.get(testCaseId);\n if (!testCase) {\n testCase = {\n id: testCaseId,\n hasFileInput: false,\n hasFileOutput: false,\n hasJudgeConfig: false,\n };\n idToTestCase.set(testCaseId, testCase);\n }\n return testCase;\n };\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n // Symbolic links were already reported for the whole problem directory.\n if (dirent.isSymbolicLink()) continue;\n // `_shared` is reserved: only `_shared.fin/` (a directory of inputs copied for every case) exists.\n if (dirent.name === SHARED_FILE_INPUT_NAME || dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)) {\n if (dirent.name !== `${SHARED_FILE_INPUT_NAME}.fin`) {\n errors.push(`test_cases/${dirent.name} is not supported; only _shared.fin/ is shared across test cases`);\n } else if (!dirent.isDirectory()) {\n errors.push(`test_cases/${dirent.name} must be a directory`);\n } else {\n const sharedFilePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (sharedFilePaths.length === 0) {\n errors.push(`test_cases/${dirent.name}/ is empty; put at least one shared input file in it`);\n }\n }\n continue;\n }\n const testCaseId = dirent.name.replace(/\\.(fin|fout)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${dirent.name} must have a test case ID before its extension`);\n continue;\n }\n if (testCaseId === dirent.name) {\n // A directory named like a file entry is a wrong-typed entry, not an ignorable stranger.\n if (dirent.isDirectory() && /\\.(in|out|json)$/.test(dirent.name)) {\n errors.push(`test_cases/${dirent.name} must be a file, not a directory`);\n }\n continue;\n }\n if (!dirent.isDirectory()) {\n errors.push(`test case ${testCaseId}: test_cases/${dirent.name} must be a directory`);\n continue;\n }\n // git does not track an empty directory, so an empty `.fin/` or `.fout/` would silently vanish on\n // commit, and the importer counts only real files that are not generated artifacts (an empty\n // subdirectory does not count); such a directory is an error and does not make the id a test case.\n const filePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (filePaths.length === 0) {\n errors.push(`test case ${testCaseId}: ${dirent.name}/ is empty; put at least one file in it or delete it`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n if (dirent.name.endsWith('.fin')) {\n testCase.hasFileInput = true;\n continue;\n }\n testCase.hasFileOutput = true;\n // The importer keeps a placeholder like any file, so the stdio judge would require the program to write it.\n for (const filePath of filePaths) {\n if (basename(filePath) === '.gitkeep') {\n warnings.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is compared as an expected output file; delete the placeholder`\n );\n }\n }\n // The stdio judge compares each expected file and refuses one above its limit (the importer rejects\n // the problem); a custom judge.ts reads the directory under its own contract.\n if (!hasCustomJudgeTs) {\n for (const filePath of filePaths) {\n const fileStats = await stat(filePath);\n if (fileStats.size > MAX_COMPARED_FILE_BYTES) {\n errors.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is larger than ${MAX_COMPARED_FILE_BYTES} bytes, which the stdio judge cannot compare`\n );\n }\n }\n }\n }\n // `_shared.*` files were already reported by the entry loop above.\n const fileNames = dirents\n .filter(\n (dirent) =>\n dirent.isFile() && /\\.(in|out|json)$/.test(dirent.name) && !dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)\n )\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const testCaseId = fileName.replace(/\\.(in|out|json)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${fileName} must have a test case ID before its extension`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n const content = await readFile(join(testCasesDirectoryPath, fileName), 'utf8');\n if (fileName.endsWith('.in')) {\n testCase.stdin = content.trimEnd();\n } else if (fileName.endsWith('.out')) {\n testCase.stdout = content.trimEnd();\n } else {\n // The presets ignore `.json`; only a custom judge.ts reads it, with a shape of its own.\n testCase.hasJudgeConfig = true;\n if (!hasCustomJudgeTs) {\n errors.push(\n `test case ${testCaseId}: ${fileName} is configuration for a custom judge.ts, which this problem does not have`\n );\n }\n try {\n JSON.parse(content);\n } catch {\n errors.push(`test case ${testCaseId}: ${fileName} is not valid JSON`);\n }\n }\n }\n\n // Directories and files are read in separate passes, so sort for deterministic per-case diagnostics below\n // (the entry-level diagnostics above follow the entry-name order of their own passes).\n const testCases = [...idToTestCase.values()].toSorted((t1, t2) => t1.id.localeCompare(t2.id));\n // A custom judge defines its own contract (e.g. it renders an output file from the input and\n // ships `.in` only), but it usually treats every case alike, so a partial set of expectations\n // still hints at a forgotten file. Input is optional in every form, so nothing guesses at it.\n // `.json` is configuration, not an output expectation, so it neither counts nor is warned about.\n const hasAnyExpectedOutput = testCases.some(hasExpectedOutput);\n for (const testCase of testCases) {\n // An empty `.fout/` was already reported above and does not count as an expectation, so a case that\n // exists through another entry is also reported here. A `.json` case without judge.ts was already\n // rejected as unsupported configuration.\n if (!hasExpectedOutput(testCase) && !testCase.hasJudgeConfig) {\n // The default stdio harness rejects such a case, so it is an error without judge.ts.\n if (!hasCustomJudgeTs) {\n if (!requiresExpectedOutput) continue;\n errors.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`\n );\n } else if (hasAnyExpectedOutput) {\n warnings.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing while other cases have one`\n );\n }\n }\n // The stdio judge rejects a run whose raw stdout exceeds the limit, so a longer expectation can\n // never match (a program may still print exactly the limit without a trailing newline).\n // A custom judge.ts reads `.out` under its own contract, so only the importer's limit applies to it.\n // The importer measures the trimmed expectation. The stdio judge measures the raw output a run\n // prints; a run matching a trimmed expectation prints at least that many characters (the token\n // comparison ignores trailing whitespace), so only a longer trimmed expectation is impossible.\n if (testCase.stdout !== undefined) {\n const maxStdoutLength = hasCustomJudgeTs ? IMPORTER_MAX_STDOUT_LENGTH : MAX_STDOUT_LENGTH;\n if (testCase.stdout.length > maxStdoutLength) {\n errors.push(\n hasCustomJudgeTs\n ? `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length} > ${maxStdoutLength})`\n : `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length}); the stdio judge rejects a run that prints more than ${maxStdoutLength} characters`\n );\n }\n }\n // The judge accepts an empty `.out` (the program must print nothing or an empty line), but it is\n // usually a stray file of a case whose stdout is not compared.\n if (testCase.stdout !== undefined && testCase.stdout.length === 0) {\n warnings.push(\n `test case ${testCase.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`\n );\n }\n }\n\n // Only stdin is compared: a case with input files may legitimately share its stdin with others,\n // and file contents are not compared for duplicates.\n const stdinToFirstId = new Map<string, string>();\n for (const testCase of testCases) {\n // An empty `.in` means \"no input\" (like an absent one) and is not a duplicate of another empty one.\n if (!testCase.stdin || testCase.hasFileInput) continue;\n const firstId = stdinToFirstId.get(testCase.stdin);\n if (firstId === undefined) {\n stdinToFirstId.set(testCase.stdin, testCase.id);\n } else {\n warnings.push(`test cases ${firstId} and ${testCase.id} have identical input`);\n }\n }\n return testCases;\n}\n\nasync function listSymbolicLinks(directoryPath: string): Promise<string[]> {\n const linkPaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isSymbolicLink()) {\n linkPaths.push(entryPath);\n } else if (dirent.isDirectory() && dirent.name !== 'node_modules') {\n linkPaths.push(...(await listSymbolicLinks(entryPath)));\n }\n }\n return linkPaths.toSorted((p1, p2) => p1.localeCompare(p2));\n}\n\n/** Lists the real files under a test case directory, leaving out generated artifacts like the importer. */\nasync function listFilesRecursively(directoryPath: string): Promise<string[]> {\n const filePaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n if (IGNORED_TEST_CASE_ENTRY_NAMES.has(dirent.name)) continue;\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isFile()) {\n filePaths.push(entryPath);\n } else if (dirent.isDirectory()) {\n filePaths.push(...(await listFilesRecursively(entryPath)));\n }\n }\n return filePaths;\n}\n\nfunction hasExpectedOutput(testCase: FileTestCase): boolean {\n return testCase.stdout !== undefined || testCase.hasFileOutput;\n}\n\nfunction validateTestCasePresence(\n frontmatter: ProblemFrontmatter | undefined,\n fileTestCases: FileTestCase[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): void {\n // Exercode's import requires test cases, a judge.ts or isManualScoringRequired and grants no\n // exemption for `type: prompt_study` or static-analysis rules (this package's `judge` subcommand\n // runs a rules-only problem, but it cannot be imported).\n if (fileTestCases.length === 0 && frontmatter?.isManualScoringRequired !== true) {\n // The judge only rejects a case-less problem when judge.ts is also missing; a special judge\n // may compute results itself, but the stdio presets need test_cases/, hence the warning.\n if (hasCustomJudgeTs) {\n warnings.push(\n 'no test cases found; fine for a special judge, but stdio-preset judges require an expected output (test_cases/<id>.out or <id>.fout/) for every case'\n );\n } else {\n errors.push(\n 'no test cases found; add test cases with an expected output (test_cases/<id>.out or <id>.fout/, plus <id>.in or <id>.fin/ when the program reads input; at least one example_* and one hidden test_*), or set isManualScoringRequired: true'\n );\n }\n }\n if (fileTestCases.length > 0) {\n reportTestCaseCompositionIssues(fileTestCases, errors, warnings);\n }\n}\n\nfunction reportTestCaseCompositionIssues(\n testCases: readonly { id: string }[],\n errors: string[],\n warnings: string[]\n): void {\n if (!testCases.some((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push('no example test case found; add at least one test case whose ID contains \"example\" (e.g. example_1)');\n }\n if (testCases.every((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push(\n 'no hidden test case found; add at least one test case whose ID does not contain \"example\" (e.g. test_1)'\n );\n }\n if (testCases.length < RECOMMENDED_MIN_TEST_CASE_COUNT) {\n warnings.push(\n `only ${testCases.length} test cases found; ${RECOMMENDED_MIN_TEST_CASE_COUNT} or more are recommended`\n );\n }\n}\n\nasync function readModelAnswers(\n problemDirectoryPath: string,\n errors: string[],\n warnings: string[]\n): Promise<ModelAnswer[]> {\n const modelAnswersDirectoryPath = join(problemDirectoryPath, 'model_answers');\n if (!(await isDirectory(modelAnswersDirectoryPath))) return [];\n\n const modelAnswers: ModelAnswer[] = [];\n const dirents = await readdir(modelAnswersDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (!dirent.isDirectory() || dirent.name === '__pycache__') continue;\n // The judge parses each directory name with learningMaterialIdSchema, so a regex-invalid\n // name (e.g. \"Python\") aborts import, while an unknown but valid ID (e.g. \"foo\") imports fine.\n if (!LEARNING_MATERIAL_ID_REGEX.test(dirent.name)) {\n errors.push(`model answer directory name \"${dirent.name}\" must match ${LEARNING_MATERIAL_ID_REGEX}`);\n continue;\n }\n if (!(availableLanguageIds as readonly string[]).includes(dirent.name)) {\n warnings.push(\n `model answer directory name \"${dirent.name}\" is not a known language ID (${availableLanguageIds.join(', ')})`\n );\n }\n const files = await readSourceFilesRecursively(join(modelAnswersDirectoryPath, dirent.name));\n // The all-problem check judges every model answer directory, so an empty one fails there.\n if (files.length === 0) {\n errors.push(`model answer directory \"${dirent.name}\" has no source files; add files or delete it`);\n continue;\n }\n modelAnswers.push({ id: dirent.name, files });\n }\n return modelAnswers;\n}\n\nfunction validateCodePatterns(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[],\n warnings: string[]\n): void {\n const requiredRegExps = compilePatterns(frontmatter.requiredRegExpsInCode, 'requiredRegExpsInCode', errors);\n const forbiddenRegExps = compilePatterns(frontmatter.forbiddenRegExpsInCode, 'forbiddenRegExpsInCode', errors);\n const forbiddenTexts = frontmatter.forbiddenTextsInCode.map((rule) => normalizeCodeRule(rule).pattern);\n\n for (const [pattern, regExp] of forbiddenRegExps) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => regExp.test(snippet))) {\n warnings.push(\n `forbiddenRegExpsInCode pattern \"${pattern}\" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n for (const text of forbiddenTexts) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => snippet.includes(text))) {\n warnings.push(\n `forbiddenTextsInCode text \"${text}\" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n\n for (const modelAnswer of modelAnswers) {\n const strippedFiles = readSourceCodeWithoutComments(modelAnswer.files);\n\n for (const [pattern, regExp] of requiredRegExps) {\n if (!strippedFiles.some((file) => regExp.test(file.data))) {\n errors.push(\n `required pattern \"${pattern}\" does not match any file of model answer \"${modelAnswer.id}\" (comments are ignored)`\n );\n }\n }\n for (const [pattern, regExp] of forbiddenRegExps) {\n for (const file of strippedFiles) {\n const matched = file.data.match(regExp);\n if (matched) {\n errors.push(\n `forbidden pattern \"${pattern}\" matches model answer file ${modelAnswer.id}/${file.path} (matched \"${matched[0]}\")`\n );\n }\n }\n }\n for (const forbiddenText of forbiddenTexts) {\n for (const file of strippedFiles) {\n if (file.data.includes(forbiddenText)) {\n errors.push(`forbidden text \"${forbiddenText}\" appears in model answer file ${modelAnswer.id}/${file.path}`);\n }\n }\n }\n }\n}\n\n/**\n * The judge rejects a submission missing a required file before running it, so every model answer\n * must ship them among the files the importer keeps (the same filter as `readSourceFilesRecursively`).\n */\nfunction validateRequiredSubmissionFiles(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[]\n): void {\n for (const modelAnswer of modelAnswers) {\n const filePathSet = new Set(modelAnswer.files.map((file) => file.path));\n for (const requiredPath of frontmatter.requiredSubmissionFilePaths) {\n if (!filePathSet.has(requiredPath)) {\n errors.push(\n `required submission file \"${requiredPath}\" is missing from model answer \"${modelAnswer.id}\" (requiredSubmissionFilePaths)`\n );\n }\n }\n }\n}\n\n/** Selects and strips the files exactly like the judge's static analysis of a submission does. */\nfunction readSourceCodeWithoutComments(files: SourceFile[]): SourceFile[] {\n return files.flatMap((file) => {\n const languageDefinition = findLanguageDefinitionByPath(file.path);\n if (!languageDefinition || file.data.includes('\\uFFFD')) return [];\n const grammar = languageDefinition.grammer;\n return [{ path: file.path, data: grammar ? removeCommentsInSourceCode(grammar, file.data) : file.data }];\n });\n}\n\nfunction compilePatterns(rules: CodeRule[], fieldName: string, errors: string[]): [string, RegExp][] {\n const compiled: [string, RegExp][] = [];\n for (const rule of rules) {\n const { pattern } = normalizeCodeRule(rule);\n try {\n compiled.push([pattern, new RegExp(pattern)]);\n } catch {\n errors.push(`invalid regular expression in ${fieldName}: ${pattern}`);\n }\n }\n return compiled;\n}\n\nasync function validateTemplates(\n problemDirectoryPath: string,\n modelAnswers: ModelAnswer[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const templatesDirectoryPath = join(problemDirectoryPath, 'templates');\n if (!(await isDirectory(templatesDirectoryPath))) return;\n\n const dirents = await readdir(templatesDirectoryPath, { withFileTypes: true });\n // The judge only skips __pycache__; a stray .DS_Store next to template directories fails\n // import as a mixed files-and-directories layout, so it must not be filtered out here.\n const entries = dirents.filter((dirent) => dirent.name !== '__pycache__');\n if (entries.some((dirent) => dirent.name === '.DS_Store')) {\n warnings.push('templates/.DS_Store found; delete it (the judge treats it as a template file)');\n }\n const hasFiles = entries.some((dirent) => dirent.isFile());\n const hasDirectories = entries.some((dirent) => dirent.isDirectory());\n if (hasFiles && hasDirectories) {\n errors.push(\n 'templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)'\n );\n return;\n }\n for (const dirent of entries) {\n if (\n dirent.isDirectory() &&\n dirent.name !== '_default' &&\n !(availableLanguageIds as readonly string[]).includes(dirent.name)\n ) {\n errors.push(\n `invalid template directory name \"${dirent.name}\"; expected a language ID (${availableLanguageIds.join(', ')}) or _default`\n );\n }\n }\n // A template must be an incomplete starting point: a template directory that carries every file\n // a model answer is judged by, unchanged, would pass the judge. The stdio judge runs the source\n // files of a supported language (notes next to the answer do not change what runs), while a\n // custom judge.ts may read any file (e.g. a CSV submission). Helper modules shared by both are fine.\n const directoryNames = entries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);\n const templateDirectoryNames = hasDirectories ? directoryNames : [''];\n for (const templateDirectoryName of templateDirectoryNames) {\n const templateFiles = await readSourceFilesRecursively(join(templatesDirectoryPath, templateDirectoryName));\n const templateContentByPath = new Map(templateFiles.map((file) => [file.path, file.data]));\n for (const modelAnswer of modelAnswers) {\n const judgedFiles = modelAnswer.files.filter(\n (file) =>\n hasCustomJudgeTs || (file.data.trim().length > 0 && findLanguageDefinitionByPath(file.path) !== undefined)\n );\n if (judgedFiles.length > 0 && judgedFiles.every((file) => templateContentByPath.get(file.path) === file.data)) {\n warnings.push(\n `templates/${templateDirectoryName} contains every judged file of model answer \"${modelAnswer.id}\" unchanged (identical to a model answer file); templates must not pass the judge`\n );\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validateProblem.cjs","names":["resolve","isRegularDirectory","isDirectory","basename","LEARNING_MATERIAL_ID_REGEX","relative","readdir","join","isRegularFile","isFile","judgesTestCasesWithoutExpectations","findLanguageDefinitionByPath","findDefaultStdioHarnessFiles","parseFrontmatter","readFile","getErrorMessage","problemFrontmatterSchema","formatZodIssues","stat","MAX_COMPARED_FILE_BYTES","MAX_STDOUT_LENGTH","EXAMPLE_TEST_CASE_ID_PATTERN","availableLanguageIds","readSourceFilesRecursively","normalizeCodeRule","removeCommentsInSourceCode","require_compareExpectedOutputFiles","require_defaultStdioHarness","require_removeCommentsInSourceCode","require_problem","require_schemas","require_fsHelpers","require_frontmatter","require_validationResult","require_validateMaterial","require_findLanguageDefinitionByPath","require_readProblemMarkdownFrontMatter","require_stdioJudgeRules","node_path","node_fs_promises","_willbooster_shared_lib","ALLOWED_PARSING_HELPERS","PARSING_CODE_SNIPPETS","SHARED_FILE_INPUT_NAME","IGNORED_TEST_CASE_ENTRY_NAMES","validateProblemDirectory","errors","warnings","result","absoluteDirectoryPath","problemDirectoryPath","problemId","linkPath","listSymbolicLinks","problemDirents","dirent","d1","d2","problemFilePath","frontmatter","parseProblemFrontmatter","harness","readHarnessFiles","requiresExpectedOutput","validateTestCasePresence","readFileTestCases","isScoredAutomatically","validateHarnessFiles","modelAnswers","readModelAnswers","usableModelAnswers","isRunnable","answer","file","validateCodePatterns","validateRequiredSubmissionFiles","validateTemplates","hasJudgeTs","defaultHarnessFileNames","hasParsedFrontmatter","attributes","body","error","parsed","testCasesDirectoryPath","dirents","idToTestCase","getTestCase","testCase","testCaseId","listFilesRecursively","filePaths","filePath","hasCustomJudgeTs","fileNames","f1","f2","fileName","content","testCases","t1","t2","hasAnyExpectedOutput","hasExpectedOutput","maxStdoutLength","stdinToFirstId","firstId","linkPaths","directoryPath","entryPath","p1","p2","fileTestCases","reportTestCaseCompositionIssues","modelAnswersDirectoryPath","files","requiredRegExps","compilePatterns","forbiddenRegExps","forbiddenTexts","rule","pattern","regExp","snippet","text","modelAnswer","strippedFiles","readSourceCodeWithoutComments","matched","forbiddenText","filePathSet","requiredPath","languageDefinition","grammar","compiled","rules","fieldName","templatesDirectoryPath","entries","hasFiles","hasDirectories","directoryNames","templateDirectoryNames","templateDirectoryName","templateFiles","templateContentByPath","judgedFiles"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { MAX_COMPARED_FILE_BYTES } from '../helpers/compareExpectedOutputFiles.js';\nimport { findDefaultStdioHarnessFiles, type HarnessFileName } from '../helpers/defaultStdioHarness.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { judgesTestCasesWithoutExpectations } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { removeCommentsInSourceCode } from '../helpers/removeCommentsInSourceCode.js';\nimport { EXAMPLE_TEST_CASE_ID_PATTERN, MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { type CodeRule, normalizeCodeRule } from '../types/problem.js';\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n readSourceFilesRecursively,\n type SourceFile,\n} from './fsHelpers.js';\nimport {\n availableLanguageIds,\n LEARNING_MATERIAL_ID_REGEX,\n type ProblemFrontmatter,\n problemFrontmatterSchema,\n} from './schemas.js';\nimport { reportExcessChatMarkers } from './validateMaterial.js';\nimport { formatZodIssues, type ValidationResult } from './validationResult.js';\n\nconst RECOMMENDED_MIN_TEST_CASE_COUNT = 4;\n// Forbidden patterns must not restrict classes/functions commonly needed just to parse stdin (from gen-em).\n// Instead of a substring check on the pattern source (\"print\" contains \"int\"), each forbidden\n// pattern is applied to representative parsing code; a hit means the pattern would forbid parsing.\nconst ALLOWED_PARSING_HELPERS = ['Scanner', 'map', 'int', 'split'] as const;\nconst PARSING_CODE_SNIPPETS = [\n 'a, b = map(int, input().split())',\n 'Scanner scanner = new Scanner(System.in);',\n \"const [a, b] = line.split(' ').map(Number);\",\n] as const;\n\ninterface FileTestCase {\n id: string;\n stdin?: string;\n stdout?: string;\n /** Whether `<id>.fin/` (input files copied into the working directory) exists and holds at least one file. */\n hasFileInput: boolean;\n /** Whether `<id>.fout/` (expected output files) exists and holds at least one file. */\n hasFileOutput: boolean;\n /** Whether `<id>.json` (configuration read by a custom `judge.ts`) exists. */\n hasJudgeConfig: boolean;\n}\n\n// The importer rejects a longer expected stdout whichever judge the problem has.\nconst IMPORTER_MAX_STDOUT_LENGTH = 100_000;\n// `_shared.fin/` holds files copied for every test case; it is not a test case itself.\nconst SHARED_FILE_INPUT_NAME = '_shared';\n// The importer leaves generated artifacts out of the packaged problem, so a directory holding only\n// these is as good as empty. Any other file counts, even a `.gitkeep` placeholder.\nconst IGNORED_TEST_CASE_ENTRY_NAMES: ReadonlySet<string> = new Set(['.DS_Store', '__pycache__']);\n\ninterface ModelAnswer {\n id: string;\n files: SourceFile[];\n}\n\n/**\n * Validates a judge v2 problem directory (problem.md, test_cases, model_answers, templates, and —\n * for custom judging or custom debugging only — judge.ts/debug.ts).\n */\nexport async function validateProblemDirectory(problemDirectoryPath: string): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const absoluteDirectoryPath = resolve(problemDirectoryPath);\n // Problem discovery does not traverse a symbolic link, so a linked problem directory is never imported.\n if (!(await isRegularDirectory(absoluteDirectoryPath))) {\n errors.push(\n (await isDirectory(absoluteDirectoryPath))\n ? `problem directory is a symbolic link, which the judge does not traverse: ${problemDirectoryPath}`\n : `problem directory not found: ${problemDirectoryPath}`\n );\n return result;\n }\n\n const problemId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(`problem ID \"${problemId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n // The judge's front-matter reader takes the first `problem.md` or `*.problem.md` it finds, so a\n // leftover v1 file can be judged instead of problem.md.\n // The judge readers enumerate regular files and directories only, so a symbolic link anywhere in\n // the problem is ignored at import time and would silently drop or change what is judged.\n for (const linkPath of await listSymbolicLinks(absoluteDirectoryPath)) {\n errors.push(\n `${relative(absoluteDirectoryPath, linkPath)} is a symbolic link, which the judge ignores; commit a real file or directory`\n );\n }\n const problemDirents = await readdir(absoluteDirectoryPath, { withFileTypes: true });\n for (const dirent of problemDirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (dirent.isFile() && dirent.name.endsWith('.problem.md')) {\n errors.push(`found v1 problem file ${dirent.name}; use the v2 layout with a file named exactly problem.md`);\n }\n }\n\n const problemFilePath = join(absoluteDirectoryPath, 'problem.md');\n // The judge discovers problems by their problem.md and skips symbolic links (reported above).\n if (!(await isRegularFile(problemFilePath))) {\n if (!(await isFile(problemFilePath))) {\n errors.push('problem.md not found; a v2 problem directory must contain problem.md');\n }\n return result;\n }\n\n const frontmatter = await parseProblemFrontmatter(problemFilePath, errors);\n const harness = await readHarnessFiles(absoluteDirectoryPath);\n const requiresExpectedOutput = frontmatter === undefined || !judgesTestCasesWithoutExpectations(frontmatter);\n const fileTestCases = await readFileTestCases(\n absoluteDirectoryPath,\n { hasCustomJudgeTs: harness.hasCustomJudgeTs, requiresExpectedOutput },\n errors,\n warnings\n );\n // A default judge.ts is about to be deleted, so treat only a genuinely custom judge.ts as a\n // special judge; otherwise the case-less error would surface only after the deletion.\n validateTestCasePresence(frontmatter, fileTestCases, harness.hasCustomJudgeTs, errors, warnings);\n\n // A problem whose frontmatter failed to parse already has a primary error; deriving secondary\n // judge.ts/model-answer errors from it would send the fix loop after the wrong problems.\n const isScoredAutomatically =\n frontmatter !== undefined && frontmatter.isManualScoringRequired !== true && frontmatter.type !== 'prompt_study';\n validateHarnessFiles(harness, frontmatter !== undefined, errors, warnings);\n\n const modelAnswers = await readModelAnswers(absoluteDirectoryPath, errors, warnings);\n // Every model answer directory is judged by the all-problem check, so each one must hold what\n // its judge runs: a custom judge defines that itself (e.g. a CSV submission), while the stdio\n // judge needs a non-empty file of a supported language as its entry point.\n const usableModelAnswers = modelAnswers.filter((answer) => {\n // A custom judge may accept an empty data file, so only the stdio judge needs content.\n if (harness.hasCustomJudgeTs) return true;\n const isRunnable = answer.files.some(\n (file) => findLanguageDefinitionByPath(file.path) !== undefined && file.data.trim().length > 0\n );\n if (!isRunnable) {\n errors.push(\n `model answer directory \"${answer.id}\" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`\n );\n }\n return isRunnable;\n });\n if (isScoredAutomatically && modelAnswers.length === 0) {\n errors.push('no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files');\n }\n if (frontmatter) {\n validateCodePatterns(frontmatter, usableModelAnswers, errors, warnings);\n validateRequiredSubmissionFiles(frontmatter, usableModelAnswers, errors);\n }\n await validateTemplates(absoluteDirectoryPath, usableModelAnswers, harness.hasCustomJudgeTs, errors, warnings);\n\n return result;\n}\n\ninterface HarnessFiles {\n hasDebugTs: boolean;\n rejectedDefaultHarnessFileNames: readonly HarnessFileName[];\n hasCustomJudgeTs: boolean;\n}\n\nasync function readHarnessFiles(absoluteDirectoryPath: string): Promise<HarnessFiles> {\n const hasJudgeTs = await isFile(join(absoluteDirectoryPath, 'judge.ts'));\n // The same policy the `judge` subcommand and the all-problem check apply: a default judge.ts is\n // always rejected, and a default debug.ts is rejected unless it accompanies a custom judge.ts.\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(absoluteDirectoryPath);\n return {\n hasDebugTs: await isFile(join(absoluteDirectoryPath, 'debug.ts')),\n rejectedDefaultHarnessFileNames: defaultHarnessFileNames,\n hasCustomJudgeTs: hasJudgeTs && !defaultHarnessFileNames.includes('judge.ts'),\n };\n}\n\n/**\n * The judge treats a problem without judge.ts as a standard stdin/stdout problem and auto-generates\n * both the stdio judge and the stdio debug runner. Committed copies of those default harnesses are\n * rejected: they add nothing and can drift from the server's defaults, and a default judge.ts also\n * hides the standard-problem marker that enables the default debug runner.\n */\nfunction validateHarnessFiles(\n harness: HarnessFiles,\n hasParsedFrontmatter: boolean,\n errors: string[],\n warnings: string[]\n): void {\n if (harness.rejectedDefaultHarnessFileNames.includes('judge.ts')) {\n errors.push(\n 'judge.ts only calls stdioJudgePreset; delete it — the judge auto-generates it, and only a problem without judge.ts is treated as a standard stdio problem (which enables the default debug runner)'\n );\n }\n if (harness.rejectedDefaultHarnessFileNames.includes('debug.ts')) {\n errors.push(\n 'debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts'\n );\n }\n // A custom debug.ts without judge.ts stays accepted: the judge runs debug.ts whenever it is\n // present (judge.ts only selects the judging side), so it is a supported way to customize\n // debugging for a problem judged by the default stdio judge.\n // The debug feature itself is independent of the scoring mode, so warn for manual-scoring and\n // prompt_study problems too.\n if (hasParsedFrontmatter && harness.hasCustomJudgeTs && !harness.hasDebugTs) {\n warnings.push(\n \"custom judge.ts without debug.ts; the debug feature is unavailable for this problem — add debug.ts (e.g. calling stdioDebugPreset from '@exercode/problem-utils/presets/stdio') or a custom debug harness\"\n );\n }\n}\n\nasync function parseProblemFrontmatter(\n problemFilePath: string,\n errors: string[]\n): Promise<ProblemFrontmatter | undefined> {\n let attributes: unknown;\n try {\n let body: string;\n ({ attributes, body } = parseFrontmatter(await readFile(problemFilePath, 'utf8')));\n // exercode rejects problem bodies with more than one standalone chat delimiter, like materials.\n reportExcessChatMarkers(body, errors);\n } catch (error) {\n errors.push(`problem.md: invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return undefined;\n }\n const parsed = problemFrontmatterSchema.safeParse(attributes);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'problem.md frontmatter'));\n return undefined;\n }\n return parsed.data;\n}\n\nasync function readFileTestCases(\n problemDirectoryPath: string,\n { hasCustomJudgeTs, requiresExpectedOutput }: { hasCustomJudgeTs: boolean; requiresExpectedOutput: boolean },\n errors: string[],\n warnings: string[]\n): Promise<FileTestCase[]> {\n const testCasesDirectoryPath = join(problemDirectoryPath, 'test_cases');\n if (!(await isDirectory(testCasesDirectoryPath))) return [];\n\n const dirents = await readdir(testCasesDirectoryPath, { withFileTypes: true });\n const idToTestCase = new Map<string, FileTestCase>();\n const getTestCase = (testCaseId: string): FileTestCase => {\n let testCase = idToTestCase.get(testCaseId);\n if (!testCase) {\n testCase = {\n id: testCaseId,\n hasFileInput: false,\n hasFileOutput: false,\n hasJudgeConfig: false,\n };\n idToTestCase.set(testCaseId, testCase);\n }\n return testCase;\n };\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n // Symbolic links were already reported for the whole problem directory.\n if (dirent.isSymbolicLink()) continue;\n // `_shared` is reserved: only `_shared.fin/` (a directory of inputs copied for every case) exists.\n if (dirent.name === SHARED_FILE_INPUT_NAME || dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)) {\n if (dirent.name !== `${SHARED_FILE_INPUT_NAME}.fin`) {\n errors.push(`test_cases/${dirent.name} is not supported; only _shared.fin/ is shared across test cases`);\n } else if (!dirent.isDirectory()) {\n errors.push(`test_cases/${dirent.name} must be a directory`);\n } else {\n const sharedFilePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (sharedFilePaths.length === 0) {\n errors.push(`test_cases/${dirent.name}/ is empty; put at least one shared input file in it`);\n }\n }\n continue;\n }\n const testCaseId = dirent.name.replace(/\\.(fin|fout)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${dirent.name} must have a test case ID before its extension`);\n continue;\n }\n if (testCaseId === dirent.name) {\n // A directory named like a file entry is a wrong-typed entry, not an ignorable stranger.\n if (dirent.isDirectory() && /\\.(in|out|json)$/.test(dirent.name)) {\n errors.push(`test_cases/${dirent.name} must be a file, not a directory`);\n }\n continue;\n }\n if (!dirent.isDirectory()) {\n errors.push(`test case ${testCaseId}: test_cases/${dirent.name} must be a directory`);\n continue;\n }\n // git does not track an empty directory, so an empty `.fin/` or `.fout/` would silently vanish on\n // commit, and the importer counts only real files that are not generated artifacts (an empty\n // subdirectory does not count); such a directory is an error and does not make the id a test case.\n const filePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (filePaths.length === 0) {\n errors.push(`test case ${testCaseId}: ${dirent.name}/ is empty; put at least one file in it or delete it`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n if (dirent.name.endsWith('.fin')) {\n testCase.hasFileInput = true;\n continue;\n }\n testCase.hasFileOutput = true;\n // The importer keeps a placeholder like any file, so the stdio judge would require the program to write it.\n for (const filePath of filePaths) {\n if (basename(filePath) === '.gitkeep') {\n warnings.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is compared as an expected output file; delete the placeholder`\n );\n }\n }\n // The stdio judge compares each expected file and refuses one above its limit (the importer rejects\n // the problem); a custom judge.ts reads the directory under its own contract.\n if (!hasCustomJudgeTs) {\n for (const filePath of filePaths) {\n const fileStats = await stat(filePath);\n if (fileStats.size > MAX_COMPARED_FILE_BYTES) {\n errors.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is larger than ${MAX_COMPARED_FILE_BYTES} bytes, which the stdio judge cannot compare`\n );\n }\n }\n }\n }\n // `_shared.*` files were already reported by the entry loop above.\n const fileNames = dirents\n .filter(\n (dirent) =>\n dirent.isFile() && /\\.(in|out|json)$/.test(dirent.name) && !dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)\n )\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const testCaseId = fileName.replace(/\\.(in|out|json)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${fileName} must have a test case ID before its extension`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n const content = await readFile(join(testCasesDirectoryPath, fileName), 'utf8');\n if (fileName.endsWith('.in')) {\n testCase.stdin = content.trimEnd();\n } else if (fileName.endsWith('.out')) {\n testCase.stdout = content.trimEnd();\n } else {\n // The presets ignore `.json`; only a custom judge.ts reads it, with a shape of its own.\n testCase.hasJudgeConfig = true;\n if (!hasCustomJudgeTs) {\n errors.push(\n `test case ${testCaseId}: ${fileName} is configuration for a custom judge.ts, which this problem does not have`\n );\n }\n try {\n JSON.parse(content);\n } catch {\n errors.push(`test case ${testCaseId}: ${fileName} is not valid JSON`);\n }\n }\n }\n\n // Directories and files are read in separate passes, so sort for deterministic per-case diagnostics below\n // (the entry-level diagnostics above follow the entry-name order of their own passes).\n const testCases = [...idToTestCase.values()].toSorted((t1, t2) => t1.id.localeCompare(t2.id));\n // A custom judge defines its own contract (e.g. it renders an output file from the input and\n // ships `.in` only), but it usually treats every case alike, so a partial set of expectations\n // still hints at a forgotten file. Input is optional in every form, so nothing guesses at it.\n // `.json` is configuration, not an output expectation, so it neither counts nor is warned about.\n const hasAnyExpectedOutput = testCases.some(hasExpectedOutput);\n for (const testCase of testCases) {\n // An empty `.fout/` was already reported above and does not count as an expectation, so a case that\n // exists through another entry is also reported here. A `.json` case without judge.ts was already\n // rejected as unsupported configuration.\n if (!hasExpectedOutput(testCase) && !testCase.hasJudgeConfig) {\n // The default stdio harness rejects such a case, so it is an error without judge.ts.\n if (!hasCustomJudgeTs) {\n if (!requiresExpectedOutput) continue;\n errors.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`\n );\n } else if (hasAnyExpectedOutput) {\n warnings.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing while other cases have one`\n );\n }\n }\n // The stdio judge rejects a run whose raw stdout exceeds the limit, so a longer expectation can\n // never match (a program may still print exactly the limit without a trailing newline).\n // A custom judge.ts reads `.out` under its own contract, so only the importer's limit applies to it.\n // The importer measures the trimmed expectation. The stdio judge measures the raw output a run\n // prints; a run matching a trimmed expectation prints at least that many characters (the token\n // comparison ignores trailing whitespace), so only a longer trimmed expectation is impossible.\n if (testCase.stdout !== undefined) {\n const maxStdoutLength = hasCustomJudgeTs ? IMPORTER_MAX_STDOUT_LENGTH : MAX_STDOUT_LENGTH;\n if (testCase.stdout.length > maxStdoutLength) {\n errors.push(\n hasCustomJudgeTs\n ? `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length} > ${maxStdoutLength})`\n : `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length}); the stdio judge rejects a run that prints more than ${maxStdoutLength} characters`\n );\n }\n }\n // The judge accepts an empty `.out` (the program must print nothing or an empty line), but it is\n // usually a stray file of a case whose stdout is not compared.\n if (testCase.stdout !== undefined && testCase.stdout.length === 0) {\n warnings.push(\n `test case ${testCase.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`\n );\n }\n }\n\n // Only stdin is compared: a case with input files may legitimately share its stdin with others,\n // and file contents are not compared for duplicates.\n const stdinToFirstId = new Map<string, string>();\n for (const testCase of testCases) {\n // An empty `.in` means \"no input\" (like an absent one) and is not a duplicate of another empty one.\n if (!testCase.stdin || testCase.hasFileInput) continue;\n const firstId = stdinToFirstId.get(testCase.stdin);\n if (firstId === undefined) {\n stdinToFirstId.set(testCase.stdin, testCase.id);\n } else {\n warnings.push(`test cases ${firstId} and ${testCase.id} have identical input`);\n }\n }\n return testCases;\n}\n\nasync function listSymbolicLinks(directoryPath: string): Promise<string[]> {\n const linkPaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isSymbolicLink()) {\n linkPaths.push(entryPath);\n } else if (dirent.isDirectory() && dirent.name !== 'node_modules') {\n linkPaths.push(...(await listSymbolicLinks(entryPath)));\n }\n }\n return linkPaths.toSorted((p1, p2) => p1.localeCompare(p2));\n}\n\n/** Lists the real files under a test case directory, leaving out generated artifacts like the importer. */\nasync function listFilesRecursively(directoryPath: string): Promise<string[]> {\n const filePaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n if (IGNORED_TEST_CASE_ENTRY_NAMES.has(dirent.name)) continue;\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isFile()) {\n filePaths.push(entryPath);\n } else if (dirent.isDirectory()) {\n filePaths.push(...(await listFilesRecursively(entryPath)));\n }\n }\n return filePaths;\n}\n\nfunction hasExpectedOutput(testCase: FileTestCase): boolean {\n return testCase.stdout !== undefined || testCase.hasFileOutput;\n}\n\nfunction validateTestCasePresence(\n frontmatter: ProblemFrontmatter | undefined,\n fileTestCases: FileTestCase[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): void {\n // Exercode's import requires test cases, a judge.ts or isManualScoringRequired and grants no\n // exemption for `type: prompt_study` or static-analysis rules (this package's `judge` subcommand\n // runs a rules-only problem, but it cannot be imported).\n if (fileTestCases.length === 0 && frontmatter?.isManualScoringRequired !== true) {\n // The judge only rejects a case-less problem when judge.ts is also missing; a special judge\n // may compute results itself, but the stdio presets need test_cases/, hence the warning.\n if (hasCustomJudgeTs) {\n warnings.push(\n 'no test cases found; fine for a special judge, but stdio-preset judges require an expected output (test_cases/<id>.out or <id>.fout/) for every case'\n );\n } else {\n errors.push(\n 'no test cases found; add test cases with an expected output (test_cases/<id>.out or <id>.fout/, plus <id>.in or <id>.fin/ when the program reads input; at least one example_* and one hidden test_*), or set isManualScoringRequired: true'\n );\n }\n }\n if (fileTestCases.length > 0) {\n reportTestCaseCompositionIssues(fileTestCases, errors, warnings);\n }\n}\n\nfunction reportTestCaseCompositionIssues(\n testCases: readonly { id: string }[],\n errors: string[],\n warnings: string[]\n): void {\n if (!testCases.some((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push('no example test case found; add at least one test case whose ID contains \"example\" (e.g. example_1)');\n }\n if (testCases.every((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push(\n 'no hidden test case found; add at least one test case whose ID does not contain \"example\" (e.g. test_1)'\n );\n }\n if (testCases.length < RECOMMENDED_MIN_TEST_CASE_COUNT) {\n warnings.push(\n `only ${testCases.length} test cases found; ${RECOMMENDED_MIN_TEST_CASE_COUNT} or more are recommended`\n );\n }\n}\n\nasync function readModelAnswers(\n problemDirectoryPath: string,\n errors: string[],\n warnings: string[]\n): Promise<ModelAnswer[]> {\n const modelAnswersDirectoryPath = join(problemDirectoryPath, 'model_answers');\n if (!(await isDirectory(modelAnswersDirectoryPath))) return [];\n\n const modelAnswers: ModelAnswer[] = [];\n const dirents = await readdir(modelAnswersDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (!dirent.isDirectory() || dirent.name === '__pycache__') continue;\n // The judge parses each directory name with learningMaterialIdSchema, so a regex-invalid\n // name (e.g. \"Python\") aborts import, while an unknown but valid ID (e.g. \"foo\") imports fine.\n if (!LEARNING_MATERIAL_ID_REGEX.test(dirent.name)) {\n errors.push(`model answer directory name \"${dirent.name}\" must match ${LEARNING_MATERIAL_ID_REGEX}`);\n continue;\n }\n if (!(availableLanguageIds as readonly string[]).includes(dirent.name)) {\n warnings.push(\n `model answer directory name \"${dirent.name}\" is not a known language ID (${availableLanguageIds.join(', ')})`\n );\n }\n const files = await readSourceFilesRecursively(join(modelAnswersDirectoryPath, dirent.name));\n // The all-problem check judges every model answer directory, so an empty one fails there.\n if (files.length === 0) {\n errors.push(`model answer directory \"${dirent.name}\" has no source files; add files or delete it`);\n continue;\n }\n modelAnswers.push({ id: dirent.name, files });\n }\n return modelAnswers;\n}\n\nfunction validateCodePatterns(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[],\n warnings: string[]\n): void {\n const requiredRegExps = compilePatterns(frontmatter.requiredRegExpsInCode, 'requiredRegExpsInCode', errors);\n const forbiddenRegExps = compilePatterns(frontmatter.forbiddenRegExpsInCode, 'forbiddenRegExpsInCode', errors);\n const forbiddenTexts = frontmatter.forbiddenTextsInCode.map((rule) => normalizeCodeRule(rule).pattern);\n\n for (const [pattern, regExp] of forbiddenRegExps) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => regExp.test(snippet))) {\n warnings.push(\n `forbiddenRegExpsInCode pattern \"${pattern}\" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n for (const text of forbiddenTexts) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => snippet.includes(text))) {\n warnings.push(\n `forbiddenTextsInCode text \"${text}\" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n\n for (const modelAnswer of modelAnswers) {\n const strippedFiles = readSourceCodeWithoutComments(modelAnswer.files);\n\n for (const [pattern, regExp] of requiredRegExps) {\n if (!strippedFiles.some((file) => regExp.test(file.data))) {\n errors.push(\n `required pattern \"${pattern}\" does not match any file of model answer \"${modelAnswer.id}\" (comments are ignored)`\n );\n }\n }\n for (const [pattern, regExp] of forbiddenRegExps) {\n for (const file of strippedFiles) {\n const matched = file.data.match(regExp);\n if (matched) {\n errors.push(\n `forbidden pattern \"${pattern}\" matches model answer file ${modelAnswer.id}/${file.path} (matched \"${matched[0]}\")`\n );\n }\n }\n }\n for (const forbiddenText of forbiddenTexts) {\n for (const file of strippedFiles) {\n if (file.data.includes(forbiddenText)) {\n errors.push(`forbidden text \"${forbiddenText}\" appears in model answer file ${modelAnswer.id}/${file.path}`);\n }\n }\n }\n }\n}\n\n/**\n * The judge rejects a submission missing a required file before running it, so every model answer\n * must ship them among the files the importer keeps (the same filter as `readSourceFilesRecursively`).\n */\nfunction validateRequiredSubmissionFiles(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[]\n): void {\n for (const modelAnswer of modelAnswers) {\n const filePathSet = new Set(modelAnswer.files.map((file) => file.path));\n for (const requiredPath of frontmatter.requiredSubmissionFilePaths) {\n if (!filePathSet.has(requiredPath)) {\n errors.push(\n `required submission file \"${requiredPath}\" is missing from model answer \"${modelAnswer.id}\" (requiredSubmissionFilePaths)`\n );\n }\n }\n }\n}\n\n/** Selects and strips the files exactly like the judge's static analysis of a submission does. */\nfunction readSourceCodeWithoutComments(files: SourceFile[]): SourceFile[] {\n return files.flatMap((file) => {\n const languageDefinition = findLanguageDefinitionByPath(file.path);\n if (!languageDefinition || file.data.includes('\\uFFFD')) return [];\n const grammar = languageDefinition.grammer;\n return [{ path: file.path, data: grammar ? removeCommentsInSourceCode(grammar, file.data) : file.data }];\n });\n}\n\nfunction compilePatterns(rules: CodeRule[], fieldName: string, errors: string[]): [string, RegExp][] {\n const compiled: [string, RegExp][] = [];\n for (const rule of rules) {\n const { pattern } = normalizeCodeRule(rule);\n try {\n compiled.push([pattern, new RegExp(pattern)]);\n } catch {\n errors.push(`invalid regular expression in ${fieldName}: ${pattern}`);\n }\n }\n return compiled;\n}\n\nasync function validateTemplates(\n problemDirectoryPath: string,\n modelAnswers: ModelAnswer[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const templatesDirectoryPath = join(problemDirectoryPath, 'templates');\n if (!(await isDirectory(templatesDirectoryPath))) return;\n\n const dirents = await readdir(templatesDirectoryPath, { withFileTypes: true });\n // The judge only skips __pycache__; a stray .DS_Store next to template directories fails\n // import as a mixed files-and-directories layout, so it must not be filtered out here.\n const entries = dirents.filter((dirent) => dirent.name !== '__pycache__');\n if (entries.some((dirent) => dirent.name === '.DS_Store')) {\n warnings.push('templates/.DS_Store found; delete it (the judge treats it as a template file)');\n }\n const hasFiles = entries.some((dirent) => dirent.isFile());\n const hasDirectories = entries.some((dirent) => dirent.isDirectory());\n if (hasFiles && hasDirectories) {\n errors.push(\n 'templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)'\n );\n return;\n }\n for (const dirent of entries) {\n if (\n dirent.isDirectory() &&\n dirent.name !== '_default' &&\n !(availableLanguageIds as readonly string[]).includes(dirent.name)\n ) {\n errors.push(\n `invalid template directory name \"${dirent.name}\"; expected a language ID (${availableLanguageIds.join(', ')}) or _default`\n );\n }\n }\n // A template must be an incomplete starting point: a template directory that carries every file\n // a model answer is judged by, unchanged, would pass the judge. The stdio judge runs the source\n // files of a supported language (notes next to the answer do not change what runs), while a\n // custom judge.ts may read any file (e.g. a CSV submission). Helper modules shared by both are fine.\n const directoryNames = entries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);\n const templateDirectoryNames = hasDirectories ? directoryNames : [''];\n for (const templateDirectoryName of templateDirectoryNames) {\n const templateFiles = await readSourceFilesRecursively(join(templatesDirectoryPath, templateDirectoryName));\n const templateContentByPath = new Map(templateFiles.map((file) => [file.path, file.data]));\n for (const modelAnswer of modelAnswers) {\n const judgedFiles = modelAnswer.files.filter(\n (file) =>\n hasCustomJudgeTs || (file.data.trim().length > 0 && findLanguageDefinitionByPath(file.path) !== undefined)\n );\n if (judgedFiles.length > 0 && judgedFiles.every((file) => templateContentByPath.get(file.path) === file.data)) {\n warnings.push(\n `templates/${templateDirectoryName} contains every judged file of model answer \"${modelAnswer.id}\" unchanged (identical to a model answer file); templates must not pass the judge`\n );\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateProblem.js","names":["ALLOWED_PARSING_HELPERS","PARSING_CODE_SNIPPETS","SHARED_FILE_INPUT_NAME","IGNORED_TEST_CASE_ENTRY_NAMES","validateProblemDirectory","errors","warnings","result","absoluteDirectoryPath","resolve","problemDirectoryPath","isRegularDirectory","isDirectory","problemId","basename","LEARNING_MATERIAL_ID_REGEX","linkPath","listSymbolicLinks","relative","problemDirents","readdir","dirent","d1","d2","problemFilePath","join","isRegularFile","isFile","frontmatter","parseProblemFrontmatter","harness","readHarnessFiles","requiresExpectedOutput","judgesTestCasesWithoutExpectations","validateTestCasePresence","readFileTestCases","isScoredAutomatically","validateHarnessFiles","modelAnswers","readModelAnswers","usableModelAnswers","isRunnable","answer","findLanguageDefinitionByPath","file","validateCodePatterns","validateRequiredSubmissionFiles","validateTemplates","hasJudgeTs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","hasParsedFrontmatter","attributes","body","parseFrontmatter","readFile","reportExcessChatMarkers","error","getErrorMessage","parsed","problemFrontmatterSchema","formatZodIssues","testCasesDirectoryPath","dirents","idToTestCase","getTestCase","testCase","testCaseId","listFilesRecursively","filePaths","filePath","hasCustomJudgeTs","stat","MAX_COMPARED_FILE_BYTES","fileNames","f1","f2","fileName","content","testCases","t1","t2","hasAnyExpectedOutput","hasExpectedOutput","maxStdoutLength","MAX_STDOUT_LENGTH","stdinToFirstId","firstId","linkPaths","directoryPath","entryPath","p1","p2","fileTestCases","reportTestCaseCompositionIssues","EXAMPLE_TEST_CASE_ID_PATTERN","modelAnswersDirectoryPath","availableLanguageIds","files","readSourceFilesRecursively","requiredRegExps","compilePatterns","forbiddenRegExps","forbiddenTexts","normalizeCodeRule","rule","pattern","regExp","snippet","text","modelAnswer","strippedFiles","readSourceCodeWithoutComments","matched","forbiddenText","filePathSet","requiredPath","languageDefinition","grammar","removeCommentsInSourceCode","compiled","rules","fieldName","templatesDirectoryPath","entries","hasFiles","hasDirectories","directoryNames","templateDirectoryNames","templateDirectoryName","templateFiles","templateContentByPath","judgedFiles"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { MAX_COMPARED_FILE_BYTES } from '../helpers/compareExpectedOutputFiles.js';\nimport { findDefaultStdioHarnessFiles, type HarnessFileName } from '../helpers/defaultStdioHarness.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { judgesTestCasesWithoutExpectations } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { removeCommentsInSourceCode } from '../helpers/removeCommentsInSourceCode.js';\nimport { EXAMPLE_TEST_CASE_ID_PATTERN, MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { type CodeRule, normalizeCodeRule } from '../types/problem.js';\n\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n readSourceFilesRecursively,\n type SourceFile,\n} from './fsHelpers.js';\nimport {\n availableLanguageIds,\n LEARNING_MATERIAL_ID_REGEX,\n problemFrontmatterSchema,\n type ProblemFrontmatter,\n} from './schemas.js';\nimport { reportExcessChatMarkers } from './validateMaterial.js';\nimport { formatZodIssues, type ValidationResult } from './validationResult.js';\n\nconst RECOMMENDED_MIN_TEST_CASE_COUNT = 4;\n// Forbidden patterns must not restrict classes/functions commonly needed just to parse stdin (from gen-em).\n// Instead of a substring check on the pattern source (\"print\" contains \"int\"), each forbidden\n// pattern is applied to representative parsing code; a hit means the pattern would forbid parsing.\nconst ALLOWED_PARSING_HELPERS = ['Scanner', 'map', 'int', 'split'] as const;\nconst PARSING_CODE_SNIPPETS = [\n 'a, b = map(int, input().split())',\n 'Scanner scanner = new Scanner(System.in);',\n \"const [a, b] = line.split(' ').map(Number);\",\n] as const;\n\ninterface FileTestCase {\n id: string;\n stdin?: string;\n stdout?: string;\n /** Whether `<id>.fin/` (input files copied into the working directory) exists and holds at least one file. */\n hasFileInput: boolean;\n /** Whether `<id>.fout/` (expected output files) exists and holds at least one file. */\n hasFileOutput: boolean;\n /** Whether `<id>.json` (configuration read by a custom `judge.ts`) exists. */\n hasJudgeConfig: boolean;\n}\n\n// The importer rejects a longer expected stdout whichever judge the problem has.\nconst IMPORTER_MAX_STDOUT_LENGTH = 100_000;\n// `_shared.fin/` holds files copied for every test case; it is not a test case itself.\nconst SHARED_FILE_INPUT_NAME = '_shared';\n// The importer leaves generated artifacts out of the packaged problem, so a directory holding only\n// these is as good as empty. Any other file counts, even a `.gitkeep` placeholder.\nconst IGNORED_TEST_CASE_ENTRY_NAMES: ReadonlySet<string> = new Set(['.DS_Store', '__pycache__']);\n\ninterface ModelAnswer {\n id: string;\n files: SourceFile[];\n}\n\n/**\n * Validates a judge v2 problem directory (problem.md, test_cases, model_answers, templates, and —\n * for custom judging or custom debugging only — judge.ts/debug.ts).\n */\nexport async function validateProblemDirectory(problemDirectoryPath: string): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const absoluteDirectoryPath = resolve(problemDirectoryPath);\n // Problem discovery does not traverse a symbolic link, so a linked problem directory is never imported.\n if (!(await isRegularDirectory(absoluteDirectoryPath))) {\n errors.push(\n (await isDirectory(absoluteDirectoryPath))\n ? `problem directory is a symbolic link, which the judge does not traverse: ${problemDirectoryPath}`\n : `problem directory not found: ${problemDirectoryPath}`\n );\n return result;\n }\n\n const problemId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(`problem ID \"${problemId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n // The judge's front-matter reader takes the first `problem.md` or `*.problem.md` it finds, so a\n // leftover v1 file can be judged instead of problem.md.\n // The judge readers enumerate regular files and directories only, so a symbolic link anywhere in\n // the problem is ignored at import time and would silently drop or change what is judged.\n for (const linkPath of await listSymbolicLinks(absoluteDirectoryPath)) {\n errors.push(\n `${relative(absoluteDirectoryPath, linkPath)} is a symbolic link, which the judge ignores; commit a real file or directory`\n );\n }\n const problemDirents = await readdir(absoluteDirectoryPath, { withFileTypes: true });\n for (const dirent of problemDirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (dirent.isFile() && dirent.name.endsWith('.problem.md')) {\n errors.push(`found v1 problem file ${dirent.name}; use the v2 layout with a file named exactly problem.md`);\n }\n }\n\n const problemFilePath = join(absoluteDirectoryPath, 'problem.md');\n // The judge discovers problems by their problem.md and skips symbolic links (reported above).\n if (!(await isRegularFile(problemFilePath))) {\n if (!(await isFile(problemFilePath))) {\n errors.push('problem.md not found; a v2 problem directory must contain problem.md');\n }\n return result;\n }\n\n const frontmatter = await parseProblemFrontmatter(problemFilePath, errors);\n const harness = await readHarnessFiles(absoluteDirectoryPath);\n const requiresExpectedOutput = frontmatter === undefined || !judgesTestCasesWithoutExpectations(frontmatter);\n const fileTestCases = await readFileTestCases(\n absoluteDirectoryPath,\n { hasCustomJudgeTs: harness.hasCustomJudgeTs, requiresExpectedOutput },\n errors,\n warnings\n );\n // A default judge.ts is about to be deleted, so treat only a genuinely custom judge.ts as a\n // special judge; otherwise the case-less error would surface only after the deletion.\n validateTestCasePresence(frontmatter, fileTestCases, harness.hasCustomJudgeTs, errors, warnings);\n\n // A problem whose frontmatter failed to parse already has a primary error; deriving secondary\n // judge.ts/model-answer errors from it would send the fix loop after the wrong problems.\n const isScoredAutomatically =\n frontmatter !== undefined && frontmatter.isManualScoringRequired !== true && frontmatter.type !== 'prompt_study';\n validateHarnessFiles(harness, frontmatter !== undefined, errors, warnings);\n\n const modelAnswers = await readModelAnswers(absoluteDirectoryPath, errors, warnings);\n // Every model answer directory is judged by the all-problem check, so each one must hold what\n // its judge runs: a custom judge defines that itself (e.g. a CSV submission), while the stdio\n // judge needs a non-empty file of a supported language as its entry point.\n const usableModelAnswers = modelAnswers.filter((answer) => {\n // A custom judge may accept an empty data file, so only the stdio judge needs content.\n if (harness.hasCustomJudgeTs) return true;\n const isRunnable = answer.files.some(\n (file) => findLanguageDefinitionByPath(file.path) !== undefined && file.data.trim().length > 0\n );\n if (!isRunnable) {\n errors.push(\n `model answer directory \"${answer.id}\" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`\n );\n }\n return isRunnable;\n });\n if (isScoredAutomatically && modelAnswers.length === 0) {\n errors.push('no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files');\n }\n if (frontmatter) {\n validateCodePatterns(frontmatter, usableModelAnswers, errors, warnings);\n validateRequiredSubmissionFiles(frontmatter, usableModelAnswers, errors);\n }\n await validateTemplates(absoluteDirectoryPath, usableModelAnswers, harness.hasCustomJudgeTs, errors, warnings);\n\n return result;\n}\n\ninterface HarnessFiles {\n hasDebugTs: boolean;\n rejectedDefaultHarnessFileNames: readonly HarnessFileName[];\n hasCustomJudgeTs: boolean;\n}\n\nasync function readHarnessFiles(absoluteDirectoryPath: string): Promise<HarnessFiles> {\n const hasJudgeTs = await isFile(join(absoluteDirectoryPath, 'judge.ts'));\n // The same policy the `judge` subcommand and the all-problem check apply: a default judge.ts is\n // always rejected, and a default debug.ts is rejected unless it accompanies a custom judge.ts.\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(absoluteDirectoryPath);\n return {\n hasDebugTs: await isFile(join(absoluteDirectoryPath, 'debug.ts')),\n rejectedDefaultHarnessFileNames: defaultHarnessFileNames,\n hasCustomJudgeTs: hasJudgeTs && !defaultHarnessFileNames.includes('judge.ts'),\n };\n}\n\n/**\n * The judge treats a problem without judge.ts as a standard stdin/stdout problem and auto-generates\n * both the stdio judge and the stdio debug runner. Committed copies of those default harnesses are\n * rejected: they add nothing and can drift from the server's defaults, and a default judge.ts also\n * hides the standard-problem marker that enables the default debug runner.\n */\nfunction validateHarnessFiles(\n harness: HarnessFiles,\n hasParsedFrontmatter: boolean,\n errors: string[],\n warnings: string[]\n): void {\n if (harness.rejectedDefaultHarnessFileNames.includes('judge.ts')) {\n errors.push(\n 'judge.ts only calls stdioJudgePreset; delete it — the judge auto-generates it, and only a problem without judge.ts is treated as a standard stdio problem (which enables the default debug runner)'\n );\n }\n if (harness.rejectedDefaultHarnessFileNames.includes('debug.ts')) {\n errors.push(\n 'debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts'\n );\n }\n // A custom debug.ts without judge.ts stays accepted: the judge runs debug.ts whenever it is\n // present (judge.ts only selects the judging side), so it is a supported way to customize\n // debugging for a problem judged by the default stdio judge.\n // The debug feature itself is independent of the scoring mode, so warn for manual-scoring and\n // prompt_study problems too.\n if (hasParsedFrontmatter && harness.hasCustomJudgeTs && !harness.hasDebugTs) {\n warnings.push(\n \"custom judge.ts without debug.ts; the debug feature is unavailable for this problem — add debug.ts (e.g. calling stdioDebugPreset from '@exercode/problem-utils/presets/stdio') or a custom debug harness\"\n );\n }\n}\n\nasync function parseProblemFrontmatter(\n problemFilePath: string,\n errors: string[]\n): Promise<ProblemFrontmatter | undefined> {\n let attributes: unknown;\n try {\n let body: string;\n ({ attributes, body } = parseFrontmatter(await readFile(problemFilePath, 'utf8')));\n // exercode rejects problem bodies with more than one standalone chat delimiter, like materials.\n reportExcessChatMarkers(body, errors);\n } catch (error) {\n errors.push(`problem.md: invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return undefined;\n }\n const parsed = problemFrontmatterSchema.safeParse(attributes);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'problem.md frontmatter'));\n return undefined;\n }\n return parsed.data;\n}\n\nasync function readFileTestCases(\n problemDirectoryPath: string,\n { hasCustomJudgeTs, requiresExpectedOutput }: { hasCustomJudgeTs: boolean; requiresExpectedOutput: boolean },\n errors: string[],\n warnings: string[]\n): Promise<FileTestCase[]> {\n const testCasesDirectoryPath = join(problemDirectoryPath, 'test_cases');\n if (!(await isDirectory(testCasesDirectoryPath))) return [];\n\n const dirents = await readdir(testCasesDirectoryPath, { withFileTypes: true });\n const idToTestCase = new Map<string, FileTestCase>();\n const getTestCase = (testCaseId: string): FileTestCase => {\n let testCase = idToTestCase.get(testCaseId);\n if (!testCase) {\n testCase = {\n id: testCaseId,\n hasFileInput: false,\n hasFileOutput: false,\n hasJudgeConfig: false,\n };\n idToTestCase.set(testCaseId, testCase);\n }\n return testCase;\n };\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n // Symbolic links were already reported for the whole problem directory.\n if (dirent.isSymbolicLink()) continue;\n // `_shared` is reserved: only `_shared.fin/` (a directory of inputs copied for every case) exists.\n if (dirent.name === SHARED_FILE_INPUT_NAME || dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)) {\n if (dirent.name !== `${SHARED_FILE_INPUT_NAME}.fin`) {\n errors.push(`test_cases/${dirent.name} is not supported; only _shared.fin/ is shared across test cases`);\n } else if (!dirent.isDirectory()) {\n errors.push(`test_cases/${dirent.name} must be a directory`);\n } else {\n const sharedFilePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (sharedFilePaths.length === 0) {\n errors.push(`test_cases/${dirent.name}/ is empty; put at least one shared input file in it`);\n }\n }\n continue;\n }\n const testCaseId = dirent.name.replace(/\\.(fin|fout)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${dirent.name} must have a test case ID before its extension`);\n continue;\n }\n if (testCaseId === dirent.name) {\n // A directory named like a file entry is a wrong-typed entry, not an ignorable stranger.\n if (dirent.isDirectory() && /\\.(in|out|json)$/.test(dirent.name)) {\n errors.push(`test_cases/${dirent.name} must be a file, not a directory`);\n }\n continue;\n }\n if (!dirent.isDirectory()) {\n errors.push(`test case ${testCaseId}: test_cases/${dirent.name} must be a directory`);\n continue;\n }\n // git does not track an empty directory, so an empty `.fin/` or `.fout/` would silently vanish on\n // commit, and the importer counts only real files that are not generated artifacts (an empty\n // subdirectory does not count); such a directory is an error and does not make the id a test case.\n const filePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (filePaths.length === 0) {\n errors.push(`test case ${testCaseId}: ${dirent.name}/ is empty; put at least one file in it or delete it`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n if (dirent.name.endsWith('.fin')) {\n testCase.hasFileInput = true;\n continue;\n }\n testCase.hasFileOutput = true;\n // The importer keeps a placeholder like any file, so the stdio judge would require the program to write it.\n for (const filePath of filePaths) {\n if (basename(filePath) === '.gitkeep') {\n warnings.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is compared as an expected output file; delete the placeholder`\n );\n }\n }\n // The stdio judge compares each expected file and refuses one above its limit (the importer rejects\n // the problem); a custom judge.ts reads the directory under its own contract.\n if (!hasCustomJudgeTs) {\n for (const filePath of filePaths) {\n const fileStats = await stat(filePath);\n if (fileStats.size > MAX_COMPARED_FILE_BYTES) {\n errors.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is larger than ${MAX_COMPARED_FILE_BYTES} bytes, which the stdio judge cannot compare`\n );\n }\n }\n }\n }\n // `_shared.*` files were already reported by the entry loop above.\n const fileNames = dirents\n .filter(\n (dirent) =>\n dirent.isFile() && /\\.(in|out|json)$/.test(dirent.name) && !dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)\n )\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const testCaseId = fileName.replace(/\\.(in|out|json)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${fileName} must have a test case ID before its extension`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n const content = await readFile(join(testCasesDirectoryPath, fileName), 'utf8');\n if (fileName.endsWith('.in')) {\n testCase.stdin = content.trimEnd();\n } else if (fileName.endsWith('.out')) {\n testCase.stdout = content.trimEnd();\n } else {\n // The presets ignore `.json`; only a custom judge.ts reads it, with a shape of its own.\n testCase.hasJudgeConfig = true;\n if (!hasCustomJudgeTs) {\n errors.push(\n `test case ${testCaseId}: ${fileName} is configuration for a custom judge.ts, which this problem does not have`\n );\n }\n try {\n JSON.parse(content);\n } catch {\n errors.push(`test case ${testCaseId}: ${fileName} is not valid JSON`);\n }\n }\n }\n\n // Directories and files are read in separate passes, so sort for deterministic per-case diagnostics below\n // (the entry-level diagnostics above follow the entry-name order of their own passes).\n const testCases = [...idToTestCase.values()].toSorted((t1, t2) => t1.id.localeCompare(t2.id));\n // A custom judge defines its own contract (e.g. it renders an output file from the input and\n // ships `.in` only), but it usually treats every case alike, so a partial set of expectations\n // still hints at a forgotten file. Input is optional in every form, so nothing guesses at it.\n // `.json` is configuration, not an output expectation, so it neither counts nor is warned about.\n const hasAnyExpectedOutput = testCases.some(hasExpectedOutput);\n for (const testCase of testCases) {\n // An empty `.fout/` was already reported above and does not count as an expectation, so a case that\n // exists through another entry is also reported here. A `.json` case without judge.ts was already\n // rejected as unsupported configuration.\n if (!hasExpectedOutput(testCase) && !testCase.hasJudgeConfig) {\n // The default stdio harness rejects such a case, so it is an error without judge.ts.\n if (!hasCustomJudgeTs) {\n if (!requiresExpectedOutput) continue;\n errors.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`\n );\n } else if (hasAnyExpectedOutput) {\n warnings.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing while other cases have one`\n );\n }\n }\n // The stdio judge rejects a run whose raw stdout exceeds the limit, so a longer expectation can\n // never match (a program may still print exactly the limit without a trailing newline).\n // A custom judge.ts reads `.out` under its own contract, so only the importer's limit applies to it.\n // The importer measures the trimmed expectation. The stdio judge measures the raw output a run\n // prints; a run matching a trimmed expectation prints at least that many characters (the token\n // comparison ignores trailing whitespace), so only a longer trimmed expectation is impossible.\n if (testCase.stdout !== undefined) {\n const maxStdoutLength = hasCustomJudgeTs ? IMPORTER_MAX_STDOUT_LENGTH : MAX_STDOUT_LENGTH;\n if (testCase.stdout.length > maxStdoutLength) {\n errors.push(\n hasCustomJudgeTs\n ? `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length} > ${maxStdoutLength})`\n : `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length}); the stdio judge rejects a run that prints more than ${maxStdoutLength} characters`\n );\n }\n }\n // The judge accepts an empty `.out` (the program must print nothing or an empty line), but it is\n // usually a stray file of a case whose stdout is not compared.\n if (testCase.stdout !== undefined && testCase.stdout.length === 0) {\n warnings.push(\n `test case ${testCase.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`\n );\n }\n }\n\n // Only stdin is compared: a case with input files may legitimately share its stdin with others,\n // and file contents are not compared for duplicates.\n const stdinToFirstId = new Map<string, string>();\n for (const testCase of testCases) {\n // An empty `.in` means \"no input\" (like an absent one) and is not a duplicate of another empty one.\n if (!testCase.stdin || testCase.hasFileInput) continue;\n const firstId = stdinToFirstId.get(testCase.stdin);\n if (firstId === undefined) {\n stdinToFirstId.set(testCase.stdin, testCase.id);\n } else {\n warnings.push(`test cases ${firstId} and ${testCase.id} have identical input`);\n }\n }\n return testCases;\n}\n\nasync function listSymbolicLinks(directoryPath: string): Promise<string[]> {\n const linkPaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isSymbolicLink()) {\n linkPaths.push(entryPath);\n } else if (dirent.isDirectory() && dirent.name !== 'node_modules') {\n linkPaths.push(...(await listSymbolicLinks(entryPath)));\n }\n }\n return linkPaths.toSorted((p1, p2) => p1.localeCompare(p2));\n}\n\n/** Lists the real files under a test case directory, leaving out generated artifacts like the importer. */\nasync function listFilesRecursively(directoryPath: string): Promise<string[]> {\n const filePaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n if (IGNORED_TEST_CASE_ENTRY_NAMES.has(dirent.name)) continue;\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isFile()) {\n filePaths.push(entryPath);\n } else if (dirent.isDirectory()) {\n filePaths.push(...(await listFilesRecursively(entryPath)));\n }\n }\n return filePaths;\n}\n\nfunction hasExpectedOutput(testCase: FileTestCase): boolean {\n return testCase.stdout !== undefined || testCase.hasFileOutput;\n}\n\nfunction validateTestCasePresence(\n frontmatter: ProblemFrontmatter | undefined,\n fileTestCases: FileTestCase[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): void {\n // Exercode's import requires test cases, a judge.ts or isManualScoringRequired and grants no\n // exemption for `type: prompt_study` or static-analysis rules (this package's `judge` subcommand\n // runs a rules-only problem, but it cannot be imported).\n if (fileTestCases.length === 0 && frontmatter?.isManualScoringRequired !== true) {\n // The judge only rejects a case-less problem when judge.ts is also missing; a special judge\n // may compute results itself, but the stdio presets need test_cases/, hence the warning.\n if (hasCustomJudgeTs) {\n warnings.push(\n 'no test cases found; fine for a special judge, but stdio-preset judges require an expected output (test_cases/<id>.out or <id>.fout/) for every case'\n );\n } else {\n errors.push(\n 'no test cases found; add test cases with an expected output (test_cases/<id>.out or <id>.fout/, plus <id>.in or <id>.fin/ when the program reads input; at least one example_* and one hidden test_*), or set isManualScoringRequired: true'\n );\n }\n }\n if (fileTestCases.length > 0) {\n reportTestCaseCompositionIssues(fileTestCases, errors, warnings);\n }\n}\n\nfunction reportTestCaseCompositionIssues(\n testCases: readonly { id: string }[],\n errors: string[],\n warnings: string[]\n): void {\n if (!testCases.some((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push('no example test case found; add at least one test case whose ID contains \"example\" (e.g. example_1)');\n }\n if (testCases.every((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push(\n 'no hidden test case found; add at least one test case whose ID does not contain \"example\" (e.g. test_1)'\n );\n }\n if (testCases.length < RECOMMENDED_MIN_TEST_CASE_COUNT) {\n warnings.push(\n `only ${testCases.length} test cases found; ${RECOMMENDED_MIN_TEST_CASE_COUNT} or more are recommended`\n );\n }\n}\n\nasync function readModelAnswers(\n problemDirectoryPath: string,\n errors: string[],\n warnings: string[]\n): Promise<ModelAnswer[]> {\n const modelAnswersDirectoryPath = join(problemDirectoryPath, 'model_answers');\n if (!(await isDirectory(modelAnswersDirectoryPath))) return [];\n\n const modelAnswers: ModelAnswer[] = [];\n const dirents = await readdir(modelAnswersDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (!dirent.isDirectory() || dirent.name === '__pycache__') continue;\n // The judge parses each directory name with learningMaterialIdSchema, so a regex-invalid\n // name (e.g. \"Python\") aborts import, while an unknown but valid ID (e.g. \"foo\") imports fine.\n if (!LEARNING_MATERIAL_ID_REGEX.test(dirent.name)) {\n errors.push(`model answer directory name \"${dirent.name}\" must match ${LEARNING_MATERIAL_ID_REGEX}`);\n continue;\n }\n if (!(availableLanguageIds as readonly string[]).includes(dirent.name)) {\n warnings.push(\n `model answer directory name \"${dirent.name}\" is not a known language ID (${availableLanguageIds.join(', ')})`\n );\n }\n const files = await readSourceFilesRecursively(join(modelAnswersDirectoryPath, dirent.name));\n // The all-problem check judges every model answer directory, so an empty one fails there.\n if (files.length === 0) {\n errors.push(`model answer directory \"${dirent.name}\" has no source files; add files or delete it`);\n continue;\n }\n modelAnswers.push({ id: dirent.name, files });\n }\n return modelAnswers;\n}\n\nfunction validateCodePatterns(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[],\n warnings: string[]\n): void {\n const requiredRegExps = compilePatterns(frontmatter.requiredRegExpsInCode, 'requiredRegExpsInCode', errors);\n const forbiddenRegExps = compilePatterns(frontmatter.forbiddenRegExpsInCode, 'forbiddenRegExpsInCode', errors);\n const forbiddenTexts = frontmatter.forbiddenTextsInCode.map((rule) => normalizeCodeRule(rule).pattern);\n\n for (const [pattern, regExp] of forbiddenRegExps) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => regExp.test(snippet))) {\n warnings.push(\n `forbiddenRegExpsInCode pattern \"${pattern}\" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n for (const text of forbiddenTexts) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => snippet.includes(text))) {\n warnings.push(\n `forbiddenTextsInCode text \"${text}\" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n\n for (const modelAnswer of modelAnswers) {\n const strippedFiles = readSourceCodeWithoutComments(modelAnswer.files);\n\n for (const [pattern, regExp] of requiredRegExps) {\n if (!strippedFiles.some((file) => regExp.test(file.data))) {\n errors.push(\n `required pattern \"${pattern}\" does not match any file of model answer \"${modelAnswer.id}\" (comments are ignored)`\n );\n }\n }\n for (const [pattern, regExp] of forbiddenRegExps) {\n for (const file of strippedFiles) {\n const matched = file.data.match(regExp);\n if (matched) {\n errors.push(\n `forbidden pattern \"${pattern}\" matches model answer file ${modelAnswer.id}/${file.path} (matched \"${matched[0]}\")`\n );\n }\n }\n }\n for (const forbiddenText of forbiddenTexts) {\n for (const file of strippedFiles) {\n if (file.data.includes(forbiddenText)) {\n errors.push(`forbidden text \"${forbiddenText}\" appears in model answer file ${modelAnswer.id}/${file.path}`);\n }\n }\n }\n }\n}\n\n/**\n * The judge rejects a submission missing a required file before running it, so every model answer\n * must ship them among the files the importer keeps (the same filter as `readSourceFilesRecursively`).\n */\nfunction validateRequiredSubmissionFiles(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[]\n): void {\n for (const modelAnswer of modelAnswers) {\n const filePathSet = new Set(modelAnswer.files.map((file) => file.path));\n for (const requiredPath of frontmatter.requiredSubmissionFilePaths) {\n if (!filePathSet.has(requiredPath)) {\n errors.push(\n `required submission file \"${requiredPath}\" is missing from model answer \"${modelAnswer.id}\" (requiredSubmissionFilePaths)`\n );\n }\n }\n }\n}\n\n/** Selects and strips the files exactly like the judge's static analysis of a submission does. */\nfunction readSourceCodeWithoutComments(files: SourceFile[]): SourceFile[] {\n return files.flatMap((file) => {\n const languageDefinition = findLanguageDefinitionByPath(file.path);\n if (!languageDefinition || file.data.includes('\\uFFFD')) return [];\n const grammar = languageDefinition.grammer;\n return [{ path: file.path, data: grammar ? removeCommentsInSourceCode(grammar, file.data) : file.data }];\n });\n}\n\nfunction compilePatterns(rules: CodeRule[], fieldName: string, errors: string[]): [string, RegExp][] {\n const compiled: [string, RegExp][] = [];\n for (const rule of rules) {\n const { pattern } = normalizeCodeRule(rule);\n try {\n compiled.push([pattern, new RegExp(pattern)]);\n } catch {\n errors.push(`invalid regular expression in ${fieldName}: ${pattern}`);\n }\n }\n return compiled;\n}\n\nasync function validateTemplates(\n problemDirectoryPath: string,\n modelAnswers: ModelAnswer[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const templatesDirectoryPath = join(problemDirectoryPath, 'templates');\n if (!(await isDirectory(templatesDirectoryPath))) return;\n\n const dirents = await readdir(templatesDirectoryPath, { withFileTypes: true });\n // The judge only skips __pycache__; a stray .DS_Store next to template directories fails\n // import as a mixed files-and-directories layout, so it must not be filtered out here.\n const entries = dirents.filter((dirent) => dirent.name !== '__pycache__');\n if (entries.some((dirent) => dirent.name === '.DS_Store')) {\n warnings.push('templates/.DS_Store found; delete it (the judge treats it as a template file)');\n }\n const hasFiles = entries.some((dirent) => dirent.isFile());\n const hasDirectories = entries.some((dirent) => dirent.isDirectory());\n if (hasFiles && hasDirectories) {\n errors.push(\n 'templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)'\n );\n return;\n }\n for (const dirent of entries) {\n if (\n dirent.isDirectory() &&\n dirent.name !== '_default' &&\n !(availableLanguageIds as readonly string[]).includes(dirent.name)\n ) {\n errors.push(\n `invalid template directory name \"${dirent.name}\"; expected a language ID (${availableLanguageIds.join(', ')}) or _default`\n );\n }\n }\n // A template must be an incomplete starting point: a template directory that carries every file\n // a model answer is judged by, unchanged, would pass the judge. The stdio judge runs the source\n // files of a supported language (notes next to the answer do not change what runs), while a\n // custom judge.ts may read any file (e.g. a CSV submission). Helper modules shared by both are fine.\n const directoryNames = entries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);\n const templateDirectoryNames = hasDirectories ? directoryNames : [''];\n for (const templateDirectoryName of templateDirectoryNames) {\n const templateFiles = await readSourceFilesRecursively(join(templatesDirectoryPath, templateDirectoryName));\n const templateContentByPath = new Map(templateFiles.map((file) => [file.path, file.data]));\n for (const modelAnswer of modelAnswers) {\n const judgedFiles = modelAnswer.files.filter(\n (file) =>\n hasCustomJudgeTs || (file.data.trim().length > 0 && findLanguageDefinitionByPath(file.path) !== undefined)\n );\n if (judgedFiles.length > 0 && judgedFiles.every((file) => templateContentByPath.get(file.path) === file.data)) {\n warnings.push(\n `templates/${templateDirectoryName} contains every judged file of model answer \"${modelAnswer.id}\" unchanged (identical to a model answer file); templates must not pass the judge`\n );\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validateProblem.js","names":["ALLOWED_PARSING_HELPERS","PARSING_CODE_SNIPPETS","SHARED_FILE_INPUT_NAME","IGNORED_TEST_CASE_ENTRY_NAMES","validateProblemDirectory","errors","warnings","result","absoluteDirectoryPath","resolve","problemDirectoryPath","isRegularDirectory","isDirectory","problemId","basename","LEARNING_MATERIAL_ID_REGEX","linkPath","listSymbolicLinks","relative","problemDirents","readdir","dirent","d1","d2","problemFilePath","join","isRegularFile","isFile","frontmatter","parseProblemFrontmatter","harness","readHarnessFiles","requiresExpectedOutput","judgesTestCasesWithoutExpectations","validateTestCasePresence","readFileTestCases","isScoredAutomatically","validateHarnessFiles","modelAnswers","readModelAnswers","usableModelAnswers","isRunnable","answer","findLanguageDefinitionByPath","file","validateCodePatterns","validateRequiredSubmissionFiles","validateTemplates","hasJudgeTs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","hasParsedFrontmatter","attributes","body","parseFrontmatter","readFile","reportExcessChatMarkers","error","getErrorMessage","parsed","problemFrontmatterSchema","formatZodIssues","testCasesDirectoryPath","dirents","idToTestCase","getTestCase","testCase","testCaseId","listFilesRecursively","filePaths","filePath","hasCustomJudgeTs","stat","MAX_COMPARED_FILE_BYTES","fileNames","f1","f2","fileName","content","testCases","t1","t2","hasAnyExpectedOutput","hasExpectedOutput","maxStdoutLength","MAX_STDOUT_LENGTH","stdinToFirstId","firstId","linkPaths","directoryPath","entryPath","p1","p2","fileTestCases","reportTestCaseCompositionIssues","EXAMPLE_TEST_CASE_ID_PATTERN","modelAnswersDirectoryPath","availableLanguageIds","files","readSourceFilesRecursively","requiredRegExps","compilePatterns","forbiddenRegExps","forbiddenTexts","normalizeCodeRule","rule","pattern","regExp","snippet","text","modelAnswer","strippedFiles","readSourceCodeWithoutComments","matched","forbiddenText","filePathSet","requiredPath","languageDefinition","grammar","removeCommentsInSourceCode","compiled","rules","fieldName","templatesDirectoryPath","entries","hasFiles","hasDirectories","directoryNames","templateDirectoryNames","templateDirectoryName","templateFiles","templateContentByPath","judgedFiles"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\n\nimport { MAX_COMPARED_FILE_BYTES } from '../helpers/compareExpectedOutputFiles.js';\nimport { findDefaultStdioHarnessFiles, type HarnessFileName } from '../helpers/defaultStdioHarness.js';\nimport { findLanguageDefinitionByPath } from '../helpers/findLanguageDefinitionByPath.js';\nimport { judgesTestCasesWithoutExpectations } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { removeCommentsInSourceCode } from '../helpers/removeCommentsInSourceCode.js';\nimport { EXAMPLE_TEST_CASE_ID_PATTERN, MAX_STDOUT_LENGTH } from '../helpers/stdioJudgeRules.js';\nimport { type CodeRule, normalizeCodeRule } from '../types/problem.js';\nimport { parseFrontmatter } from './frontmatter.js';\nimport {\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n readSourceFilesRecursively,\n type SourceFile,\n} from './fsHelpers.js';\nimport {\n availableLanguageIds,\n LEARNING_MATERIAL_ID_REGEX,\n type ProblemFrontmatter,\n problemFrontmatterSchema,\n} from './schemas.js';\nimport { reportExcessChatMarkers } from './validateMaterial.js';\nimport { formatZodIssues, type ValidationResult } from './validationResult.js';\n\nconst RECOMMENDED_MIN_TEST_CASE_COUNT = 4;\n// Forbidden patterns must not restrict classes/functions commonly needed just to parse stdin (from gen-em).\n// Instead of a substring check on the pattern source (\"print\" contains \"int\"), each forbidden\n// pattern is applied to representative parsing code; a hit means the pattern would forbid parsing.\nconst ALLOWED_PARSING_HELPERS = ['Scanner', 'map', 'int', 'split'] as const;\nconst PARSING_CODE_SNIPPETS = [\n 'a, b = map(int, input().split())',\n 'Scanner scanner = new Scanner(System.in);',\n \"const [a, b] = line.split(' ').map(Number);\",\n] as const;\n\ninterface FileTestCase {\n id: string;\n stdin?: string;\n stdout?: string;\n /** Whether `<id>.fin/` (input files copied into the working directory) exists and holds at least one file. */\n hasFileInput: boolean;\n /** Whether `<id>.fout/` (expected output files) exists and holds at least one file. */\n hasFileOutput: boolean;\n /** Whether `<id>.json` (configuration read by a custom `judge.ts`) exists. */\n hasJudgeConfig: boolean;\n}\n\n// The importer rejects a longer expected stdout whichever judge the problem has.\nconst IMPORTER_MAX_STDOUT_LENGTH = 100_000;\n// `_shared.fin/` holds files copied for every test case; it is not a test case itself.\nconst SHARED_FILE_INPUT_NAME = '_shared';\n// The importer leaves generated artifacts out of the packaged problem, so a directory holding only\n// these is as good as empty. Any other file counts, even a `.gitkeep` placeholder.\nconst IGNORED_TEST_CASE_ENTRY_NAMES: ReadonlySet<string> = new Set(['.DS_Store', '__pycache__']);\n\ninterface ModelAnswer {\n id: string;\n files: SourceFile[];\n}\n\n/**\n * Validates a judge v2 problem directory (problem.md, test_cases, model_answers, templates, and —\n * for custom judging or custom debugging only — judge.ts/debug.ts).\n */\nexport async function validateProblemDirectory(problemDirectoryPath: string): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n const absoluteDirectoryPath = resolve(problemDirectoryPath);\n // Problem discovery does not traverse a symbolic link, so a linked problem directory is never imported.\n if (!(await isRegularDirectory(absoluteDirectoryPath))) {\n errors.push(\n (await isDirectory(absoluteDirectoryPath))\n ? `problem directory is a symbolic link, which the judge does not traverse: ${problemDirectoryPath}`\n : `problem directory not found: ${problemDirectoryPath}`\n );\n return result;\n }\n\n const problemId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(`problem ID \"${problemId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n // The judge's front-matter reader takes the first `problem.md` or `*.problem.md` it finds, so a\n // leftover v1 file can be judged instead of problem.md.\n // The judge readers enumerate regular files and directories only, so a symbolic link anywhere in\n // the problem is ignored at import time and would silently drop or change what is judged.\n for (const linkPath of await listSymbolicLinks(absoluteDirectoryPath)) {\n errors.push(\n `${relative(absoluteDirectoryPath, linkPath)} is a symbolic link, which the judge ignores; commit a real file or directory`\n );\n }\n const problemDirents = await readdir(absoluteDirectoryPath, { withFileTypes: true });\n for (const dirent of problemDirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (dirent.isFile() && dirent.name.endsWith('.problem.md')) {\n errors.push(`found v1 problem file ${dirent.name}; use the v2 layout with a file named exactly problem.md`);\n }\n }\n\n const problemFilePath = join(absoluteDirectoryPath, 'problem.md');\n // The judge discovers problems by their problem.md and skips symbolic links (reported above).\n if (!(await isRegularFile(problemFilePath))) {\n if (!(await isFile(problemFilePath))) {\n errors.push('problem.md not found; a v2 problem directory must contain problem.md');\n }\n return result;\n }\n\n const frontmatter = await parseProblemFrontmatter(problemFilePath, errors);\n const harness = await readHarnessFiles(absoluteDirectoryPath);\n const requiresExpectedOutput = frontmatter === undefined || !judgesTestCasesWithoutExpectations(frontmatter);\n const fileTestCases = await readFileTestCases(\n absoluteDirectoryPath,\n { hasCustomJudgeTs: harness.hasCustomJudgeTs, requiresExpectedOutput },\n errors,\n warnings\n );\n // A default judge.ts is about to be deleted, so treat only a genuinely custom judge.ts as a\n // special judge; otherwise the case-less error would surface only after the deletion.\n validateTestCasePresence(frontmatter, fileTestCases, harness.hasCustomJudgeTs, errors, warnings);\n\n // A problem whose frontmatter failed to parse already has a primary error; deriving secondary\n // judge.ts/model-answer errors from it would send the fix loop after the wrong problems.\n const isScoredAutomatically =\n frontmatter !== undefined && frontmatter.isManualScoringRequired !== true && frontmatter.type !== 'prompt_study';\n validateHarnessFiles(harness, frontmatter !== undefined, errors, warnings);\n\n const modelAnswers = await readModelAnswers(absoluteDirectoryPath, errors, warnings);\n // Every model answer directory is judged by the all-problem check, so each one must hold what\n // its judge runs: a custom judge defines that itself (e.g. a CSV submission), while the stdio\n // judge needs a non-empty file of a supported language as its entry point.\n const usableModelAnswers = modelAnswers.filter((answer) => {\n // A custom judge may accept an empty data file, so only the stdio judge needs content.\n if (harness.hasCustomJudgeTs) return true;\n const isRunnable = answer.files.some(\n (file) => findLanguageDefinitionByPath(file.path) !== undefined && file.data.trim().length > 0\n );\n if (!isRunnable) {\n errors.push(\n `model answer directory \"${answer.id}\" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`\n );\n }\n return isRunnable;\n });\n if (isScoredAutomatically && modelAnswers.length === 0) {\n errors.push('no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files');\n }\n if (frontmatter) {\n validateCodePatterns(frontmatter, usableModelAnswers, errors, warnings);\n validateRequiredSubmissionFiles(frontmatter, usableModelAnswers, errors);\n }\n await validateTemplates(absoluteDirectoryPath, usableModelAnswers, harness.hasCustomJudgeTs, errors, warnings);\n\n return result;\n}\n\ninterface HarnessFiles {\n hasDebugTs: boolean;\n rejectedDefaultHarnessFileNames: readonly HarnessFileName[];\n hasCustomJudgeTs: boolean;\n}\n\nasync function readHarnessFiles(absoluteDirectoryPath: string): Promise<HarnessFiles> {\n const hasJudgeTs = await isFile(join(absoluteDirectoryPath, 'judge.ts'));\n // The same policy the `judge` subcommand and the all-problem check apply: a default judge.ts is\n // always rejected, and a default debug.ts is rejected unless it accompanies a custom judge.ts.\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(absoluteDirectoryPath);\n return {\n hasDebugTs: await isFile(join(absoluteDirectoryPath, 'debug.ts')),\n rejectedDefaultHarnessFileNames: defaultHarnessFileNames,\n hasCustomJudgeTs: hasJudgeTs && !defaultHarnessFileNames.includes('judge.ts'),\n };\n}\n\n/**\n * The judge treats a problem without judge.ts as a standard stdin/stdout problem and auto-generates\n * both the stdio judge and the stdio debug runner. Committed copies of those default harnesses are\n * rejected: they add nothing and can drift from the server's defaults, and a default judge.ts also\n * hides the standard-problem marker that enables the default debug runner.\n */\nfunction validateHarnessFiles(\n harness: HarnessFiles,\n hasParsedFrontmatter: boolean,\n errors: string[],\n warnings: string[]\n): void {\n if (harness.rejectedDefaultHarnessFileNames.includes('judge.ts')) {\n errors.push(\n 'judge.ts only calls stdioJudgePreset; delete it — the judge auto-generates it, and only a problem without judge.ts is treated as a standard stdio problem (which enables the default debug runner)'\n );\n }\n if (harness.rejectedDefaultHarnessFileNames.includes('debug.ts')) {\n errors.push(\n 'debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts'\n );\n }\n // A custom debug.ts without judge.ts stays accepted: the judge runs debug.ts whenever it is\n // present (judge.ts only selects the judging side), so it is a supported way to customize\n // debugging for a problem judged by the default stdio judge.\n // The debug feature itself is independent of the scoring mode, so warn for manual-scoring and\n // prompt_study problems too.\n if (hasParsedFrontmatter && harness.hasCustomJudgeTs && !harness.hasDebugTs) {\n warnings.push(\n \"custom judge.ts without debug.ts; the debug feature is unavailable for this problem — add debug.ts (e.g. calling stdioDebugPreset from '@exercode/problem-utils/presets/stdio') or a custom debug harness\"\n );\n }\n}\n\nasync function parseProblemFrontmatter(\n problemFilePath: string,\n errors: string[]\n): Promise<ProblemFrontmatter | undefined> {\n let attributes: unknown;\n try {\n let body: string;\n ({ attributes, body } = parseFrontmatter(await readFile(problemFilePath, 'utf8')));\n // exercode rejects problem bodies with more than one standalone chat delimiter, like materials.\n reportExcessChatMarkers(body, errors);\n } catch (error) {\n errors.push(`problem.md: invalid YAML frontmatter: ${getErrorMessage(error)}`);\n return undefined;\n }\n const parsed = problemFrontmatterSchema.safeParse(attributes);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'problem.md frontmatter'));\n return undefined;\n }\n return parsed.data;\n}\n\nasync function readFileTestCases(\n problemDirectoryPath: string,\n { hasCustomJudgeTs, requiresExpectedOutput }: { hasCustomJudgeTs: boolean; requiresExpectedOutput: boolean },\n errors: string[],\n warnings: string[]\n): Promise<FileTestCase[]> {\n const testCasesDirectoryPath = join(problemDirectoryPath, 'test_cases');\n if (!(await isDirectory(testCasesDirectoryPath))) return [];\n\n const dirents = await readdir(testCasesDirectoryPath, { withFileTypes: true });\n const idToTestCase = new Map<string, FileTestCase>();\n const getTestCase = (testCaseId: string): FileTestCase => {\n let testCase = idToTestCase.get(testCaseId);\n if (!testCase) {\n testCase = {\n id: testCaseId,\n hasFileInput: false,\n hasFileOutput: false,\n hasJudgeConfig: false,\n };\n idToTestCase.set(testCaseId, testCase);\n }\n return testCase;\n };\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n // Symbolic links were already reported for the whole problem directory.\n if (dirent.isSymbolicLink()) continue;\n // `_shared` is reserved: only `_shared.fin/` (a directory of inputs copied for every case) exists.\n if (dirent.name === SHARED_FILE_INPUT_NAME || dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)) {\n if (dirent.name !== `${SHARED_FILE_INPUT_NAME}.fin`) {\n errors.push(`test_cases/${dirent.name} is not supported; only _shared.fin/ is shared across test cases`);\n } else if (!dirent.isDirectory()) {\n errors.push(`test_cases/${dirent.name} must be a directory`);\n } else {\n const sharedFilePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (sharedFilePaths.length === 0) {\n errors.push(`test_cases/${dirent.name}/ is empty; put at least one shared input file in it`);\n }\n }\n continue;\n }\n const testCaseId = dirent.name.replace(/\\.(fin|fout)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${dirent.name} must have a test case ID before its extension`);\n continue;\n }\n if (testCaseId === dirent.name) {\n // A directory named like a file entry is a wrong-typed entry, not an ignorable stranger.\n if (dirent.isDirectory() && /\\.(in|out|json)$/.test(dirent.name)) {\n errors.push(`test_cases/${dirent.name} must be a file, not a directory`);\n }\n continue;\n }\n if (!dirent.isDirectory()) {\n errors.push(`test case ${testCaseId}: test_cases/${dirent.name} must be a directory`);\n continue;\n }\n // git does not track an empty directory, so an empty `.fin/` or `.fout/` would silently vanish on\n // commit, and the importer counts only real files that are not generated artifacts (an empty\n // subdirectory does not count); such a directory is an error and does not make the id a test case.\n const filePaths = await listFilesRecursively(join(testCasesDirectoryPath, dirent.name));\n if (filePaths.length === 0) {\n errors.push(`test case ${testCaseId}: ${dirent.name}/ is empty; put at least one file in it or delete it`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n if (dirent.name.endsWith('.fin')) {\n testCase.hasFileInput = true;\n continue;\n }\n testCase.hasFileOutput = true;\n // The importer keeps a placeholder like any file, so the stdio judge would require the program to write it.\n for (const filePath of filePaths) {\n if (basename(filePath) === '.gitkeep') {\n warnings.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is compared as an expected output file; delete the placeholder`\n );\n }\n }\n // The stdio judge compares each expected file and refuses one above its limit (the importer rejects\n // the problem); a custom judge.ts reads the directory under its own contract.\n if (!hasCustomJudgeTs) {\n for (const filePath of filePaths) {\n const fileStats = await stat(filePath);\n if (fileStats.size > MAX_COMPARED_FILE_BYTES) {\n errors.push(\n `test case ${testCaseId}: ${dirent.name}/${relative(join(testCasesDirectoryPath, dirent.name), filePath)} is larger than ${MAX_COMPARED_FILE_BYTES} bytes, which the stdio judge cannot compare`\n );\n }\n }\n }\n }\n // `_shared.*` files were already reported by the entry loop above.\n const fileNames = dirents\n .filter(\n (dirent) =>\n dirent.isFile() && /\\.(in|out|json)$/.test(dirent.name) && !dirent.name.startsWith(`${SHARED_FILE_INPUT_NAME}.`)\n )\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const testCaseId = fileName.replace(/\\.(in|out|json)$/, '');\n if (testCaseId === '') {\n errors.push(`test_cases/${fileName} must have a test case ID before its extension`);\n continue;\n }\n const testCase = getTestCase(testCaseId);\n const content = await readFile(join(testCasesDirectoryPath, fileName), 'utf8');\n if (fileName.endsWith('.in')) {\n testCase.stdin = content.trimEnd();\n } else if (fileName.endsWith('.out')) {\n testCase.stdout = content.trimEnd();\n } else {\n // The presets ignore `.json`; only a custom judge.ts reads it, with a shape of its own.\n testCase.hasJudgeConfig = true;\n if (!hasCustomJudgeTs) {\n errors.push(\n `test case ${testCaseId}: ${fileName} is configuration for a custom judge.ts, which this problem does not have`\n );\n }\n try {\n JSON.parse(content);\n } catch {\n errors.push(`test case ${testCaseId}: ${fileName} is not valid JSON`);\n }\n }\n }\n\n // Directories and files are read in separate passes, so sort for deterministic per-case diagnostics below\n // (the entry-level diagnostics above follow the entry-name order of their own passes).\n const testCases = [...idToTestCase.values()].toSorted((t1, t2) => t1.id.localeCompare(t2.id));\n // A custom judge defines its own contract (e.g. it renders an output file from the input and\n // ships `.in` only), but it usually treats every case alike, so a partial set of expectations\n // still hints at a forgotten file. Input is optional in every form, so nothing guesses at it.\n // `.json` is configuration, not an output expectation, so it neither counts nor is warned about.\n const hasAnyExpectedOutput = testCases.some(hasExpectedOutput);\n for (const testCase of testCases) {\n // An empty `.fout/` was already reported above and does not count as an expectation, so a case that\n // exists through another entry is also reported here. A `.json` case without judge.ts was already\n // rejected as unsupported configuration.\n if (!hasExpectedOutput(testCase) && !testCase.hasJudgeConfig) {\n // The default stdio harness rejects such a case, so it is an error without judge.ts.\n if (!hasCustomJudgeTs) {\n if (!requiresExpectedOutput) continue;\n errors.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`\n );\n } else if (hasAnyExpectedOutput) {\n warnings.push(\n `test case ${testCase.id}: ${testCase.id}.out (or ${testCase.id}.fout/) is missing while other cases have one`\n );\n }\n }\n // The stdio judge rejects a run whose raw stdout exceeds the limit, so a longer expectation can\n // never match (a program may still print exactly the limit without a trailing newline).\n // A custom judge.ts reads `.out` under its own contract, so only the importer's limit applies to it.\n // The importer measures the trimmed expectation. The stdio judge measures the raw output a run\n // prints; a run matching a trimmed expectation prints at least that many characters (the token\n // comparison ignores trailing whitespace), so only a longer trimmed expectation is impossible.\n if (testCase.stdout !== undefined) {\n const maxStdoutLength = hasCustomJudgeTs ? IMPORTER_MAX_STDOUT_LENGTH : MAX_STDOUT_LENGTH;\n if (testCase.stdout.length > maxStdoutLength) {\n errors.push(\n hasCustomJudgeTs\n ? `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length} > ${maxStdoutLength})`\n : `test case ${testCase.id}: .out is too large (length: ${testCase.stdout.length}); the stdio judge rejects a run that prints more than ${maxStdoutLength} characters`\n );\n }\n }\n // The judge accepts an empty `.out` (the program must print nothing or an empty line), but it is\n // usually a stray file of a case whose stdout is not compared.\n if (testCase.stdout !== undefined && testCase.stdout.length === 0) {\n warnings.push(\n `test case ${testCase.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`\n );\n }\n }\n\n // Only stdin is compared: a case with input files may legitimately share its stdin with others,\n // and file contents are not compared for duplicates.\n const stdinToFirstId = new Map<string, string>();\n for (const testCase of testCases) {\n // An empty `.in` means \"no input\" (like an absent one) and is not a duplicate of another empty one.\n if (!testCase.stdin || testCase.hasFileInput) continue;\n const firstId = stdinToFirstId.get(testCase.stdin);\n if (firstId === undefined) {\n stdinToFirstId.set(testCase.stdin, testCase.id);\n } else {\n warnings.push(`test cases ${firstId} and ${testCase.id} have identical input`);\n }\n }\n return testCases;\n}\n\nasync function listSymbolicLinks(directoryPath: string): Promise<string[]> {\n const linkPaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isSymbolicLink()) {\n linkPaths.push(entryPath);\n } else if (dirent.isDirectory() && dirent.name !== 'node_modules') {\n linkPaths.push(...(await listSymbolicLinks(entryPath)));\n }\n }\n return linkPaths.toSorted((p1, p2) => p1.localeCompare(p2));\n}\n\n/** Lists the real files under a test case directory, leaving out generated artifacts like the importer. */\nasync function listFilesRecursively(directoryPath: string): Promise<string[]> {\n const filePaths: string[] = [];\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n if (IGNORED_TEST_CASE_ENTRY_NAMES.has(dirent.name)) continue;\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isFile()) {\n filePaths.push(entryPath);\n } else if (dirent.isDirectory()) {\n filePaths.push(...(await listFilesRecursively(entryPath)));\n }\n }\n return filePaths;\n}\n\nfunction hasExpectedOutput(testCase: FileTestCase): boolean {\n return testCase.stdout !== undefined || testCase.hasFileOutput;\n}\n\nfunction validateTestCasePresence(\n frontmatter: ProblemFrontmatter | undefined,\n fileTestCases: FileTestCase[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): void {\n // Exercode's import requires test cases, a judge.ts or isManualScoringRequired and grants no\n // exemption for `type: prompt_study` or static-analysis rules (this package's `judge` subcommand\n // runs a rules-only problem, but it cannot be imported).\n if (fileTestCases.length === 0 && frontmatter?.isManualScoringRequired !== true) {\n // The judge only rejects a case-less problem when judge.ts is also missing; a special judge\n // may compute results itself, but the stdio presets need test_cases/, hence the warning.\n if (hasCustomJudgeTs) {\n warnings.push(\n 'no test cases found; fine for a special judge, but stdio-preset judges require an expected output (test_cases/<id>.out or <id>.fout/) for every case'\n );\n } else {\n errors.push(\n 'no test cases found; add test cases with an expected output (test_cases/<id>.out or <id>.fout/, plus <id>.in or <id>.fin/ when the program reads input; at least one example_* and one hidden test_*), or set isManualScoringRequired: true'\n );\n }\n }\n if (fileTestCases.length > 0) {\n reportTestCaseCompositionIssues(fileTestCases, errors, warnings);\n }\n}\n\nfunction reportTestCaseCompositionIssues(\n testCases: readonly { id: string }[],\n errors: string[],\n warnings: string[]\n): void {\n if (!testCases.some((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push('no example test case found; add at least one test case whose ID contains \"example\" (e.g. example_1)');\n }\n if (testCases.every((testCase) => EXAMPLE_TEST_CASE_ID_PATTERN.test(testCase.id))) {\n errors.push(\n 'no hidden test case found; add at least one test case whose ID does not contain \"example\" (e.g. test_1)'\n );\n }\n if (testCases.length < RECOMMENDED_MIN_TEST_CASE_COUNT) {\n warnings.push(\n `only ${testCases.length} test cases found; ${RECOMMENDED_MIN_TEST_CASE_COUNT} or more are recommended`\n );\n }\n}\n\nasync function readModelAnswers(\n problemDirectoryPath: string,\n errors: string[],\n warnings: string[]\n): Promise<ModelAnswer[]> {\n const modelAnswersDirectoryPath = join(problemDirectoryPath, 'model_answers');\n if (!(await isDirectory(modelAnswersDirectoryPath))) return [];\n\n const modelAnswers: ModelAnswer[] = [];\n const dirents = await readdir(modelAnswersDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents.toSorted((d1, d2) => d1.name.localeCompare(d2.name))) {\n if (!dirent.isDirectory() || dirent.name === '__pycache__') continue;\n // The judge parses each directory name with learningMaterialIdSchema, so a regex-invalid\n // name (e.g. \"Python\") aborts import, while an unknown but valid ID (e.g. \"foo\") imports fine.\n if (!LEARNING_MATERIAL_ID_REGEX.test(dirent.name)) {\n errors.push(`model answer directory name \"${dirent.name}\" must match ${LEARNING_MATERIAL_ID_REGEX}`);\n continue;\n }\n if (!(availableLanguageIds as readonly string[]).includes(dirent.name)) {\n warnings.push(\n `model answer directory name \"${dirent.name}\" is not a known language ID (${availableLanguageIds.join(', ')})`\n );\n }\n const files = await readSourceFilesRecursively(join(modelAnswersDirectoryPath, dirent.name));\n // The all-problem check judges every model answer directory, so an empty one fails there.\n if (files.length === 0) {\n errors.push(`model answer directory \"${dirent.name}\" has no source files; add files or delete it`);\n continue;\n }\n modelAnswers.push({ id: dirent.name, files });\n }\n return modelAnswers;\n}\n\nfunction validateCodePatterns(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[],\n warnings: string[]\n): void {\n const requiredRegExps = compilePatterns(frontmatter.requiredRegExpsInCode, 'requiredRegExpsInCode', errors);\n const forbiddenRegExps = compilePatterns(frontmatter.forbiddenRegExpsInCode, 'forbiddenRegExpsInCode', errors);\n const forbiddenTexts = frontmatter.forbiddenTextsInCode.map((rule) => normalizeCodeRule(rule).pattern);\n\n for (const [pattern, regExp] of forbiddenRegExps) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => regExp.test(snippet))) {\n warnings.push(\n `forbiddenRegExpsInCode pattern \"${pattern}\" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n for (const text of forbiddenTexts) {\n if (PARSING_CODE_SNIPPETS.some((snippet) => snippet.includes(text))) {\n warnings.push(\n `forbiddenTextsInCode text \"${text}\" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${ALLOWED_PARSING_HELPERS.join(', ')})`\n );\n }\n }\n\n for (const modelAnswer of modelAnswers) {\n const strippedFiles = readSourceCodeWithoutComments(modelAnswer.files);\n\n for (const [pattern, regExp] of requiredRegExps) {\n if (!strippedFiles.some((file) => regExp.test(file.data))) {\n errors.push(\n `required pattern \"${pattern}\" does not match any file of model answer \"${modelAnswer.id}\" (comments are ignored)`\n );\n }\n }\n for (const [pattern, regExp] of forbiddenRegExps) {\n for (const file of strippedFiles) {\n const matched = file.data.match(regExp);\n if (matched) {\n errors.push(\n `forbidden pattern \"${pattern}\" matches model answer file ${modelAnswer.id}/${file.path} (matched \"${matched[0]}\")`\n );\n }\n }\n }\n for (const forbiddenText of forbiddenTexts) {\n for (const file of strippedFiles) {\n if (file.data.includes(forbiddenText)) {\n errors.push(`forbidden text \"${forbiddenText}\" appears in model answer file ${modelAnswer.id}/${file.path}`);\n }\n }\n }\n }\n}\n\n/**\n * The judge rejects a submission missing a required file before running it, so every model answer\n * must ship them among the files the importer keeps (the same filter as `readSourceFilesRecursively`).\n */\nfunction validateRequiredSubmissionFiles(\n frontmatter: ProblemFrontmatter,\n modelAnswers: ModelAnswer[],\n errors: string[]\n): void {\n for (const modelAnswer of modelAnswers) {\n const filePathSet = new Set(modelAnswer.files.map((file) => file.path));\n for (const requiredPath of frontmatter.requiredSubmissionFilePaths) {\n if (!filePathSet.has(requiredPath)) {\n errors.push(\n `required submission file \"${requiredPath}\" is missing from model answer \"${modelAnswer.id}\" (requiredSubmissionFilePaths)`\n );\n }\n }\n }\n}\n\n/** Selects and strips the files exactly like the judge's static analysis of a submission does. */\nfunction readSourceCodeWithoutComments(files: SourceFile[]): SourceFile[] {\n return files.flatMap((file) => {\n const languageDefinition = findLanguageDefinitionByPath(file.path);\n if (!languageDefinition || file.data.includes('\\uFFFD')) return [];\n const grammar = languageDefinition.grammer;\n return [{ path: file.path, data: grammar ? removeCommentsInSourceCode(grammar, file.data) : file.data }];\n });\n}\n\nfunction compilePatterns(rules: CodeRule[], fieldName: string, errors: string[]): [string, RegExp][] {\n const compiled: [string, RegExp][] = [];\n for (const rule of rules) {\n const { pattern } = normalizeCodeRule(rule);\n try {\n compiled.push([pattern, new RegExp(pattern)]);\n } catch {\n errors.push(`invalid regular expression in ${fieldName}: ${pattern}`);\n }\n }\n return compiled;\n}\n\nasync function validateTemplates(\n problemDirectoryPath: string,\n modelAnswers: ModelAnswer[],\n hasCustomJudgeTs: boolean,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const templatesDirectoryPath = join(problemDirectoryPath, 'templates');\n if (!(await isDirectory(templatesDirectoryPath))) return;\n\n const dirents = await readdir(templatesDirectoryPath, { withFileTypes: true });\n // The judge only skips __pycache__; a stray .DS_Store next to template directories fails\n // import as a mixed files-and-directories layout, so it must not be filtered out here.\n const entries = dirents.filter((dirent) => dirent.name !== '__pycache__');\n if (entries.some((dirent) => dirent.name === '.DS_Store')) {\n warnings.push('templates/.DS_Store found; delete it (the judge treats it as a template file)');\n }\n const hasFiles = entries.some((dirent) => dirent.isFile());\n const hasDirectories = entries.some((dirent) => dirent.isDirectory());\n if (hasFiles && hasDirectories) {\n errors.push(\n 'templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)'\n );\n return;\n }\n for (const dirent of entries) {\n if (\n dirent.isDirectory() &&\n dirent.name !== '_default' &&\n !(availableLanguageIds as readonly string[]).includes(dirent.name)\n ) {\n errors.push(\n `invalid template directory name \"${dirent.name}\"; expected a language ID (${availableLanguageIds.join(', ')}) or _default`\n );\n }\n }\n // A template must be an incomplete starting point: a template directory that carries every file\n // a model answer is judged by, unchanged, would pass the judge. The stdio judge runs the source\n // files of a supported language (notes next to the answer do not change what runs), while a\n // custom judge.ts may read any file (e.g. a CSV submission). Helper modules shared by both are fine.\n const directoryNames = entries.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);\n const templateDirectoryNames = hasDirectories ? directoryNames : [''];\n for (const templateDirectoryName of templateDirectoryNames) {\n const templateFiles = await readSourceFilesRecursively(join(templatesDirectoryPath, templateDirectoryName));\n const templateContentByPath = new Map(templateFiles.map((file) => [file.path, file.data]));\n for (const modelAnswer of modelAnswers) {\n const judgedFiles = modelAnswer.files.filter(\n (file) =>\n hasCustomJudgeTs || (file.data.trim().length > 0 && findLanguageDefinitionByPath(file.path) !== undefined)\n );\n if (judgedFiles.length > 0 && judgedFiles.every((file) => templateContentByPath.get(file.path) === file.data)) {\n warnings.push(\n `templates/${templateDirectoryName} contains every judged file of model answer \"${modelAnswer.id}\" unchanged (identical to a model answer file); templates must not pass the judge`\n );\n }\n }\n }\n}\n"],"mappings":"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"}
|