@exercode/problem-utils 4.2.6 → 4.3.0

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.
Files changed (90) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.js +1 -1
  4. package/dist/cli/checkAllProblems.js.map +1 -1
  5. package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
  6. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  7. package/dist/helpers/checkProblemDirIsolation.js +1 -1
  8. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  9. package/dist/helpers/findModelAnswerDirs.cjs +1 -1
  10. package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
  11. package/dist/helpers/findModelAnswerDirs.js +1 -1
  12. package/dist/helpers/findModelAnswerDirs.js.map +1 -1
  13. package/dist/helpers/guiRecording.cjs +2 -0
  14. package/dist/helpers/guiRecording.cjs.map +1 -0
  15. package/dist/helpers/guiRecording.d.ts +32 -0
  16. package/dist/helpers/guiRecording.js +2 -0
  17. package/dist/helpers/guiRecording.js.map +1 -0
  18. package/dist/helpers/judgeByStaticAnalysis.cjs +1 -1
  19. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  20. package/dist/helpers/judgeByStaticAnalysis.js +1 -1
  21. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  22. package/dist/helpers/readTestCases.cjs +1 -1
  23. package/dist/helpers/readTestCases.cjs.map +1 -1
  24. package/dist/helpers/readTestCases.js +1 -1
  25. package/dist/helpers/readTestCases.js.map +1 -1
  26. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
  27. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  28. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
  29. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  30. package/dist/helpers/runHarnessProcess.cjs +1 -1
  31. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  32. package/dist/helpers/runHarnessProcess.js +1 -1
  33. package/dist/helpers/runHarnessProcess.js.map +1 -1
  34. package/dist/helpers/spawnWithLimits.cjs +1 -1
  35. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  36. package/dist/helpers/spawnWithLimits.js +1 -1
  37. package/dist/helpers/spawnWithLimits.js.map +1 -1
  38. package/dist/index.cjs +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +1 -1
  41. package/dist/learningMaterial/fsHelpers.cjs +1 -1
  42. package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
  43. package/dist/learningMaterial/fsHelpers.js +1 -1
  44. package/dist/learningMaterial/fsHelpers.js.map +1 -1
  45. package/dist/learningMaterial/validateContest.cjs +1 -1
  46. package/dist/learningMaterial/validateContest.cjs.map +1 -1
  47. package/dist/learningMaterial/validateContest.js +1 -1
  48. package/dist/learningMaterial/validateContest.js.map +1 -1
  49. package/dist/learningMaterial/validateCourse.cjs +1 -1
  50. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  51. package/dist/learningMaterial/validateCourse.js +1 -1
  52. package/dist/learningMaterial/validateCourse.js.map +1 -1
  53. package/dist/learningMaterial/validateMaterial.cjs +3 -3
  54. package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
  55. package/dist/learningMaterial/validateMaterial.js +3 -3
  56. package/dist/learningMaterial/validateMaterial.js.map +1 -1
  57. package/dist/learningMaterial/validateProblem.cjs +1 -1
  58. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  59. package/dist/learningMaterial/validateProblem.js +1 -1
  60. package/dist/learningMaterial/validateProblem.js.map +1 -1
  61. package/dist/learningMaterial/validationResult.cjs +1 -1
  62. package/dist/learningMaterial/validationResult.cjs.map +1 -1
  63. package/dist/learningMaterial/validationResult.js +1 -1
  64. package/dist/learningMaterial/validationResult.js.map +1 -1
  65. package/dist/presets/command.cjs +1 -1
  66. package/dist/presets/command.cjs.map +1 -1
  67. package/dist/presets/command.js +1 -1
  68. package/dist/presets/command.js.map +1 -1
  69. package/dist/presets/evaluation.cjs +2 -2
  70. package/dist/presets/evaluation.cjs.map +1 -1
  71. package/dist/presets/evaluation.js +2 -2
  72. package/dist/presets/evaluation.js.map +1 -1
  73. package/dist/presets/guiCommand.cjs +3 -3
  74. package/dist/presets/guiCommand.cjs.map +1 -1
  75. package/dist/presets/guiCommand.d.ts +17 -3
  76. package/dist/presets/guiCommand.js +3 -3
  77. package/dist/presets/guiCommand.js.map +1 -1
  78. package/dist/presets/springBoot.cjs +4 -4
  79. package/dist/presets/springBoot.cjs.map +1 -1
  80. package/dist/presets/springBoot.js +4 -4
  81. package/dist/presets/springBoot.js.map +1 -1
  82. package/dist/presets/stdio.cjs +1 -1
  83. package/dist/presets/stdio.cjs.map +1 -1
  84. package/dist/presets/stdio.js +1 -1
  85. package/dist/presets/stdio.js.map +1 -1
  86. package/dist/presets/tomcat.cjs +4 -4
  87. package/dist/presets/tomcat.cjs.map +1 -1
  88. package/dist/presets/tomcat.js +4 -4
  89. package/dist/presets/tomcat.js.map +1 -1
  90. package/package.json +3 -2
@@ -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","problemFrontmatterSchema","formatZodIssues","stat","MAX_COMPARED_FILE_BYTES","MAX_STDOUT_LENGTH","EXAMPLE_TEST_CASE_ID_PATTERN","availableLanguageIds","readSourceFilesRecursively","normalizeCodeRule","removeCommentsInSourceCode"],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\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: ${error instanceof Error ? error.message : String(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"],"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,2 +1,2 @@
1
- import{MAX_COMPARED_FILE_BYTES as e}from"../helpers/compareExpectedOutputFiles.js";import{findDefaultStdioHarnessFiles as t}from"../helpers/defaultStdioHarness.js";import{removeCommentsInSourceCode as n}from"../helpers/removeCommentsInSourceCode.js";import{normalizeCodeRule as r}from"../types/problem.js";import{LEARNING_MATERIAL_ID_REGEX as i,availableLanguageIds as a,problemFrontmatterSchema as o}from"./schemas.js";import{isDirectory as s,isFile as c,isRegularDirectory as l,isRegularFile as u,readSourceFilesRecursively as d}from"./fsHelpers.js";import{parseFrontmatter as f}from"./frontmatter.js";import{formatZodIssues as p}from"./validationResult.js";import{reportExcessChatMarkers as m}from"./validateMaterial.js";import{findLanguageDefinitionByPath as h}from"../helpers/findLanguageDefinitionByPath.js";import{judgesTestCasesWithoutExpectations as g}from"../helpers/readProblemMarkdownFrontMatter.js";import{EXAMPLE_TEST_CASE_ID_PATTERN as _,MAX_STDOUT_LENGTH as v}from"../helpers/stdioJudgeRules.js";import{basename as y,join as b,relative as x,resolve as S}from"node:path";import{readFile as C,readdir as w,stat as T}from"node:fs/promises";const E=[`Scanner`,`map`,`int`,`split`],D=[`a, b = map(int, input().split())`,`Scanner scanner = new Scanner(System.in);`,`const [a, b] = line.split(' ').map(Number);`],O=`_shared`,k=new Set([`.DS_Store`,`__pycache__`]);async function A(e){let t=[],n=[],r={errors:t,warnings:n},a=S(e);if(!await l(a))return t.push(await s(a)?`problem directory is a symbolic link, which the judge does not traverse: ${e}`:`problem directory not found: ${e}`),r;let o=y(a);i.test(o)||t.push(`problem ID "${o}" (directory name) must match ${i}`);for(let e of await F(a))t.push(`${x(a,e)} is a symbolic link, which the judge ignores; commit a real file or directory`);let d=await w(a,{withFileTypes:!0});for(let e of d.toSorted((e,t)=>e.name.localeCompare(t.name)))e.isFile()&&e.name.endsWith(`.problem.md`)&&t.push(`found v1 problem file ${e.name}; use the v2 layout with a file named exactly problem.md`);let f=b(a,`problem.md`);if(!await u(f))return await c(f)||t.push(`problem.md not found; a v2 problem directory must contain problem.md`),r;let p=await N(f,t),m=await j(a),_=p===void 0||!g(p);R(p,await P(a,{hasCustomJudgeTs:m.hasCustomJudgeTs,requiresExpectedOutput:_},t,n),m.hasCustomJudgeTs,t,n);let v=p!==void 0&&p.isManualScoringRequired!==!0&&p.type!==`prompt_study`;M(m,p!==void 0,t,n);let C=await B(a,t,n),T=C.filter(e=>{if(m.hasCustomJudgeTs)return!0;let n=e.files.some(e=>h(e.path)!==void 0&&e.data.trim().length>0);return n||t.push(`model answer directory "${e.id}" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`),n});return v&&C.length===0&&t.push(`no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files`),p&&(V(p,T,t,n),H(p,T,t)),await G(a,T,m.hasCustomJudgeTs,t,n),r}async function j(e){let n=await c(b(e,`judge.ts`)),r=await t(e);return{hasDebugTs:await c(b(e,`debug.ts`)),rejectedDefaultHarnessFileNames:r,hasCustomJudgeTs:n&&!r.includes(`judge.ts`)}}function M(e,t,n,r){e.rejectedDefaultHarnessFileNames.includes(`judge.ts`)&&n.push(`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)`),e.rejectedDefaultHarnessFileNames.includes(`debug.ts`)&&n.push(`debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts`),t&&e.hasCustomJudgeTs&&!e.hasDebugTs&&r.push(`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`)}async function N(e,t){let n;try{let r;({attributes:n,body:r}=f(await C(e,`utf8`))),m(r,t)}catch(e){t.push(`problem.md: invalid YAML frontmatter: ${e instanceof Error?e.message:String(e)}`);return}let r=o.safeParse(n);if(!r.success){t.push(...p(r.error,`problem.md frontmatter`));return}return r.data}async function P(t,{hasCustomJudgeTs:n,requiresExpectedOutput:r},i,a){let o=b(t,`test_cases`);if(!await s(o))return[];let c=await w(o,{withFileTypes:!0}),l=new Map,u=e=>{let t=l.get(e);return t||(t={id:e,hasFileInput:!1,hasFileOutput:!1,hasJudgeConfig:!1},l.set(e,t)),t};for(let t of c.toSorted((e,t)=>e.name.localeCompare(t.name))){if(t.isSymbolicLink())continue;if(t.name===O||t.name.startsWith(`${O}.`)){t.name===`${O}.fin`?t.isDirectory()?(await I(b(o,t.name))).length===0&&i.push(`test_cases/${t.name}/ is empty; put at least one shared input file in it`):i.push(`test_cases/${t.name} must be a directory`):i.push(`test_cases/${t.name} is not supported; only _shared.fin/ is shared across test cases`);continue}let r=t.name.replace(/\.(fin|fout)$/,``);if(r===``){i.push(`test_cases/${t.name} must have a test case ID before its extension`);continue}if(r===t.name){t.isDirectory()&&/\.(in|out|json)$/.test(t.name)&&i.push(`test_cases/${t.name} must be a file, not a directory`);continue}if(!t.isDirectory()){i.push(`test case ${r}: test_cases/${t.name} must be a directory`);continue}let s=await I(b(o,t.name));if(s.length===0){i.push(`test case ${r}: ${t.name}/ is empty; put at least one file in it or delete it`);continue}let c=u(r);if(t.name.endsWith(`.fin`)){c.hasFileInput=!0;continue}c.hasFileOutput=!0;for(let e of s)y(e)===`.gitkeep`&&a.push(`test case ${r}: ${t.name}/${x(b(o,t.name),e)} is compared as an expected output file; delete the placeholder`);if(!n)for(let n of s)(await T(n)).size>8388608&&i.push(`test case ${r}: ${t.name}/${x(b(o,t.name),n)} is larger than ${e} bytes, which the stdio judge cannot compare`)}let d=c.filter(e=>e.isFile()&&/\.(in|out|json)$/.test(e.name)&&!e.name.startsWith(`${O}.`)).map(e=>e.name).toSorted((e,t)=>e.localeCompare(t));for(let e of d){let t=e.replace(/\.(in|out|json)$/,``);if(t===``){i.push(`test_cases/${e} must have a test case ID before its extension`);continue}let r=u(t),a=await C(b(o,e),`utf8`);if(e.endsWith(`.in`))r.stdin=a.trimEnd();else if(e.endsWith(`.out`))r.stdout=a.trimEnd();else{r.hasJudgeConfig=!0,n||i.push(`test case ${t}: ${e} is configuration for a custom judge.ts, which this problem does not have`);try{JSON.parse(a)}catch{i.push(`test case ${t}: ${e} is not valid JSON`)}}}let f=[...l.values()].toSorted((e,t)=>e.id.localeCompare(t.id)),p=f.some(L);for(let e of f){if(!L(e)&&!e.hasJudgeConfig){if(n)p&&a.push(`test case ${e.id}: ${e.id}.out (or ${e.id}.fout/) is missing while other cases have one`);else{if(!r)continue;i.push(`test case ${e.id}: ${e.id}.out (or ${e.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`)}}if(e.stdout!==void 0){let t=n?1e5:v;e.stdout.length>t&&i.push(n?`test case ${e.id}: .out is too large (length: ${e.stdout.length} > ${t})`:`test case ${e.id}: .out is too large (length: ${e.stdout.length}); the stdio judge rejects a run that prints more than ${t} characters`)}e.stdout!==void 0&&e.stdout.length===0&&a.push(`test case ${e.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`)}let m=new Map;for(let e of f){if(!e.stdin||e.hasFileInput)continue;let t=m.get(e.stdin);t===void 0?m.set(e.stdin,e.id):a.push(`test cases ${t} and ${e.id} have identical input`)}return f}async function F(e){let t=[];for(let n of await w(e,{withFileTypes:!0})){let r=b(e,n.name);n.isSymbolicLink()?t.push(r):n.isDirectory()&&n.name!==`node_modules`&&t.push(...await F(r))}return t.toSorted((e,t)=>e.localeCompare(t))}async function I(e){let t=[];for(let n of await w(e,{withFileTypes:!0})){if(k.has(n.name))continue;let r=b(e,n.name);n.isFile()?t.push(r):n.isDirectory()&&t.push(...await I(r))}return t}function L(e){return e.stdout!==void 0||e.hasFileOutput}function R(e,t,n,r,i){t.length===0&&e?.isManualScoringRequired!==!0&&(n?i.push(`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`):r.push(`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`)),t.length>0&&z(t,r,i)}function z(e,t,n){e.some(e=>_.test(e.id))||t.push(`no example test case found; add at least one test case whose ID contains "example" (e.g. example_1)`),e.every(e=>_.test(e.id))&&t.push(`no hidden test case found; add at least one test case whose ID does not contain "example" (e.g. test_1)`),e.length<4&&n.push(`only ${e.length} test cases found; 4 or more are recommended`)}async function B(e,t,n){let r=b(e,`model_answers`);if(!await s(r))return[];let o=[],c=await w(r,{withFileTypes:!0});for(let e of c.toSorted((e,t)=>e.name.localeCompare(t.name))){if(!e.isDirectory()||e.name===`__pycache__`)continue;if(!i.test(e.name)){t.push(`model answer directory name "${e.name}" must match ${i}`);continue}a.includes(e.name)||n.push(`model answer directory name "${e.name}" is not a known language ID (${a.join(`, `)})`);let s=await d(b(r,e.name));if(s.length===0){t.push(`model answer directory "${e.name}" has no source files; add files or delete it`);continue}o.push({id:e.name,files:s})}return o}function V(e,t,n,i){let a=W(e.requiredRegExpsInCode,`requiredRegExpsInCode`,n),o=W(e.forbiddenRegExpsInCode,`forbiddenRegExpsInCode`,n),s=e.forbiddenTextsInCode.map(e=>r(e).pattern);for(let[e,t]of o)D.some(e=>t.test(e))&&i.push(`forbiddenRegExpsInCode pattern "${e}" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${E.join(`, `)})`);for(let e of s)D.some(t=>t.includes(e))&&i.push(`forbiddenTextsInCode text "${e}" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${E.join(`, `)})`);for(let e of t){let t=U(e.files);for(let[r,i]of a)t.some(e=>i.test(e.data))||n.push(`required pattern "${r}" does not match any file of model answer "${e.id}" (comments are ignored)`);for(let[r,i]of o)for(let a of t){let t=a.data.match(i);t&&n.push(`forbidden pattern "${r}" matches model answer file ${e.id}/${a.path} (matched "${t[0]}")`)}for(let r of s)for(let i of t)i.data.includes(r)&&n.push(`forbidden text "${r}" appears in model answer file ${e.id}/${i.path}`)}}function H(e,t,n){for(let r of t){let t=new Set(r.files.map(e=>e.path));for(let i of e.requiredSubmissionFilePaths)t.has(i)||n.push(`required submission file "${i}" is missing from model answer "${r.id}" (requiredSubmissionFilePaths)`)}}function U(e){return e.flatMap(e=>{let t=h(e.path);if(!t||e.data.includes(`�`))return[];let r=t.grammer;return[{path:e.path,data:r?n(r,e.data):e.data}]})}function W(e,t,n){let i=[];for(let a of e){let{pattern:e}=r(a);try{i.push([e,new RegExp(e)])}catch{n.push(`invalid regular expression in ${t}: ${e}`)}}return i}async function G(e,t,n,r,i){let o=b(e,`templates`);if(!await s(o))return;let c=(await w(o,{withFileTypes:!0})).filter(e=>e.name!==`__pycache__`);c.some(e=>e.name===`.DS_Store`)&&i.push(`templates/.DS_Store found; delete it (the judge treats it as a template file)`);let l=c.some(e=>e.isFile()),u=c.some(e=>e.isDirectory());if(l&&u){r.push(`templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)`);return}for(let e of c)e.isDirectory()&&e.name!==`_default`&&!a.includes(e.name)&&r.push(`invalid template directory name "${e.name}"; expected a language ID (${a.join(`, `)}) or _default`);let f=c.filter(e=>e.isDirectory()).map(e=>e.name),p=u?f:[``];for(let e of p){let r=await d(b(o,e)),a=new Map(r.map(e=>[e.path,e.data]));for(let r of t){let t=r.files.filter(e=>n||e.data.trim().length>0&&h(e.path)!==void 0);t.length>0&&t.every(e=>a.get(e.path)===e.data)&&i.push(`templates/${e} contains every judged file of model answer "${r.id}" unchanged (identical to a model answer file); templates must not pass the judge`)}}}export{A as validateProblemDirectory};
1
+ import{MAX_COMPARED_FILE_BYTES as e}from"../helpers/compareExpectedOutputFiles.js";import{findDefaultStdioHarnessFiles as t}from"../helpers/defaultStdioHarness.js";import{removeCommentsInSourceCode as n}from"../helpers/removeCommentsInSourceCode.js";import{normalizeCodeRule as r}from"../types/problem.js";import{LEARNING_MATERIAL_ID_REGEX as i,availableLanguageIds as a,problemFrontmatterSchema as o}from"./schemas.js";import{isDirectory as s,isFile as c,isRegularDirectory as l,isRegularFile as u,readSourceFilesRecursively as d}from"./fsHelpers.js";import{parseFrontmatter as f}from"./frontmatter.js";import{formatZodIssues as p}from"./validationResult.js";import{reportExcessChatMarkers as m}from"./validateMaterial.js";import{findLanguageDefinitionByPath as h}from"../helpers/findLanguageDefinitionByPath.js";import{judgesTestCasesWithoutExpectations as g}from"../helpers/readProblemMarkdownFrontMatter.js";import{EXAMPLE_TEST_CASE_ID_PATTERN as _,MAX_STDOUT_LENGTH as v}from"../helpers/stdioJudgeRules.js";import{basename as y,join as b,relative as x,resolve as S}from"node:path";import{readFile as C,readdir as w,stat as T}from"node:fs/promises";import{getErrorMessage as E}from"@willbooster/shared-lib";const D=[`Scanner`,`map`,`int`,`split`],O=[`a, b = map(int, input().split())`,`Scanner scanner = new Scanner(System.in);`,`const [a, b] = line.split(' ').map(Number);`],k=`_shared`,A=/* @__PURE__ */ new Set([`.DS_Store`,`__pycache__`]);async function j(e){let t=[],n=[],r={errors:t,warnings:n},a=S(e);if(!await l(a))return t.push(await s(a)?`problem directory is a symbolic link, which the judge does not traverse: ${e}`:`problem directory not found: ${e}`),r;let o=y(a);i.test(o)||t.push(`problem ID "${o}" (directory name) must match ${i}`);for(let e of await I(a))t.push(`${x(a,e)} is a symbolic link, which the judge ignores; commit a real file or directory`);let d=await w(a,{withFileTypes:!0});for(let e of d.toSorted((e,t)=>e.name.localeCompare(t.name)))e.isFile()&&e.name.endsWith(`.problem.md`)&&t.push(`found v1 problem file ${e.name}; use the v2 layout with a file named exactly problem.md`);let f=b(a,`problem.md`);if(!await u(f))return await c(f)||t.push(`problem.md not found; a v2 problem directory must contain problem.md`),r;let p=await P(f,t),m=await M(a),_=p===void 0||!g(p);z(p,await F(a,{hasCustomJudgeTs:m.hasCustomJudgeTs,requiresExpectedOutput:_},t,n),m.hasCustomJudgeTs,t,n);let v=p!==void 0&&p.isManualScoringRequired!==!0&&p.type!==`prompt_study`;N(m,p!==void 0,t,n);let C=await V(a,t,n),T=C.filter(e=>{if(m.hasCustomJudgeTs)return!0;let n=e.files.some(e=>h(e.path)!==void 0&&e.data.trim().length>0);return n||t.push(`model answer directory "${e.id}" has no non-empty source file of a supported language, which the stdio judge needs as its entry point`),n});return v&&C.length===0&&t.push(`no model answers found; add at least one model_answers/<languageId>/ directory with non-empty files`),p&&(H(p,T,t,n),U(p,T,t)),await K(a,T,m.hasCustomJudgeTs,t,n),r}async function M(e){let n=await c(b(e,`judge.ts`)),r=await t(e);return{hasDebugTs:await c(b(e,`debug.ts`)),rejectedDefaultHarnessFileNames:r,hasCustomJudgeTs:n&&!r.includes(`judge.ts`)}}function N(e,t,n,r){e.rejectedDefaultHarnessFileNames.includes(`judge.ts`)&&n.push(`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)`),e.rejectedDefaultHarnessFileNames.includes(`debug.ts`)&&n.push(`debug.ts only calls stdioDebugPreset; delete it — the judge auto-generates it for problems without judge.ts`),t&&e.hasCustomJudgeTs&&!e.hasDebugTs&&r.push(`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`)}async function P(e,t){let n;try{let r;({attributes:n,body:r}=f(await C(e,`utf8`))),m(r,t)}catch(e){t.push(`problem.md: invalid YAML frontmatter: ${E(e)}`);return}let r=o.safeParse(n);if(!r.success){t.push(...p(r.error,`problem.md frontmatter`));return}return r.data}async function F(t,{hasCustomJudgeTs:n,requiresExpectedOutput:r},i,a){let o=b(t,`test_cases`);if(!await s(o))return[];let c=await w(o,{withFileTypes:!0}),l=/* @__PURE__ */ new Map,u=e=>{let t=l.get(e);return t||(t={id:e,hasFileInput:!1,hasFileOutput:!1,hasJudgeConfig:!1},l.set(e,t)),t};for(let t of c.toSorted((e,t)=>e.name.localeCompare(t.name))){if(t.isSymbolicLink())continue;if(t.name===k||t.name.startsWith(`${k}.`)){t.name===`${k}.fin`?t.isDirectory()?(await L(b(o,t.name))).length===0&&i.push(`test_cases/${t.name}/ is empty; put at least one shared input file in it`):i.push(`test_cases/${t.name} must be a directory`):i.push(`test_cases/${t.name} is not supported; only _shared.fin/ is shared across test cases`);continue}let r=t.name.replace(/\.(fin|fout)$/,``);if(r===``){i.push(`test_cases/${t.name} must have a test case ID before its extension`);continue}if(r===t.name){t.isDirectory()&&/\.(in|out|json)$/.test(t.name)&&i.push(`test_cases/${t.name} must be a file, not a directory`);continue}if(!t.isDirectory()){i.push(`test case ${r}: test_cases/${t.name} must be a directory`);continue}let s=await L(b(o,t.name));if(s.length===0){i.push(`test case ${r}: ${t.name}/ is empty; put at least one file in it or delete it`);continue}let c=u(r);if(t.name.endsWith(`.fin`)){c.hasFileInput=!0;continue}c.hasFileOutput=!0;for(let e of s)y(e)===`.gitkeep`&&a.push(`test case ${r}: ${t.name}/${x(b(o,t.name),e)} is compared as an expected output file; delete the placeholder`);if(!n)for(let n of s)(await T(n)).size>8388608&&i.push(`test case ${r}: ${t.name}/${x(b(o,t.name),n)} is larger than ${e} bytes, which the stdio judge cannot compare`)}let d=c.filter(e=>e.isFile()&&/\.(in|out|json)$/.test(e.name)&&!e.name.startsWith(`${k}.`)).map(e=>e.name).toSorted((e,t)=>e.localeCompare(t));for(let e of d){let t=e.replace(/\.(in|out|json)$/,``);if(t===``){i.push(`test_cases/${e} must have a test case ID before its extension`);continue}let r=u(t),a=await C(b(o,e),`utf8`);if(e.endsWith(`.in`))r.stdin=a.trimEnd();else if(e.endsWith(`.out`))r.stdout=a.trimEnd();else{r.hasJudgeConfig=!0,n||i.push(`test case ${t}: ${e} is configuration for a custom judge.ts, which this problem does not have`);try{JSON.parse(a)}catch{i.push(`test case ${t}: ${e} is not valid JSON`)}}}let f=[...l.values()].toSorted((e,t)=>e.id.localeCompare(t.id)),p=f.some(R);for(let e of f){if(!R(e)&&!e.hasJudgeConfig){if(n)p&&a.push(`test case ${e.id}: ${e.id}.out (or ${e.id}.fout/) is missing while other cases have one`);else{if(!r)continue;i.push(`test case ${e.id}: ${e.id}.out (or ${e.id}.fout/) is missing; a problem without judge.ts needs an expected output for every test case`)}}if(e.stdout!==void 0){let t=n?1e5:v;e.stdout.length>t&&i.push(n?`test case ${e.id}: .out is too large (length: ${e.stdout.length} > ${t})`:`test case ${e.id}: .out is too large (length: ${e.stdout.length}); the stdio judge rejects a run that prints more than ${t} characters`)}e.stdout!==void 0&&e.stdout.length===0&&a.push(`test case ${e.id}: .out is empty, so only a program that prints nothing (or an empty line) is accepted; delete .out when stdout is not compared`)}let m=/* @__PURE__ */ new Map;for(let e of f){if(!e.stdin||e.hasFileInput)continue;let t=m.get(e.stdin);t===void 0?m.set(e.stdin,e.id):a.push(`test cases ${t} and ${e.id} have identical input`)}return f}async function I(e){let t=[];for(let n of await w(e,{withFileTypes:!0})){let r=b(e,n.name);n.isSymbolicLink()?t.push(r):n.isDirectory()&&n.name!==`node_modules`&&t.push(...await I(r))}return t.toSorted((e,t)=>e.localeCompare(t))}async function L(e){let t=[];for(let n of await w(e,{withFileTypes:!0})){if(A.has(n.name))continue;let r=b(e,n.name);n.isFile()?t.push(r):n.isDirectory()&&t.push(...await L(r))}return t}function R(e){return e.stdout!==void 0||e.hasFileOutput}function z(e,t,n,r,i){t.length===0&&e?.isManualScoringRequired!==!0&&(n?i.push(`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`):r.push(`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`)),t.length>0&&B(t,r,i)}function B(e,t,n){e.some(e=>_.test(e.id))||t.push(`no example test case found; add at least one test case whose ID contains "example" (e.g. example_1)`),e.every(e=>_.test(e.id))&&t.push(`no hidden test case found; add at least one test case whose ID does not contain "example" (e.g. test_1)`),e.length<4&&n.push(`only ${e.length} test cases found; 4 or more are recommended`)}async function V(e,t,n){let r=b(e,`model_answers`);if(!await s(r))return[];let o=[],c=await w(r,{withFileTypes:!0});for(let e of c.toSorted((e,t)=>e.name.localeCompare(t.name))){if(!e.isDirectory()||e.name===`__pycache__`)continue;if(!i.test(e.name)){t.push(`model answer directory name "${e.name}" must match ${i}`);continue}a.includes(e.name)||n.push(`model answer directory name "${e.name}" is not a known language ID (${a.join(`, `)})`);let s=await d(b(r,e.name));if(s.length===0){t.push(`model answer directory "${e.name}" has no source files; add files or delete it`);continue}o.push({id:e.name,files:s})}return o}function H(e,t,n,i){let a=G(e.requiredRegExpsInCode,`requiredRegExpsInCode`,n),o=G(e.forbiddenRegExpsInCode,`forbiddenRegExpsInCode`,n),s=e.forbiddenTextsInCode.map(e=>r(e).pattern);for(let[e,t]of o)O.some(e=>t.test(e))&&i.push(`forbiddenRegExpsInCode pattern "${e}" matches typical stdin-parsing code; patterns must not restrict parsing helpers (${D.join(`, `)})`);for(let e of s)O.some(t=>t.includes(e))&&i.push(`forbiddenTextsInCode text "${e}" appears in typical stdin-parsing code; texts must not restrict parsing helpers (${D.join(`, `)})`);for(let e of t){let t=W(e.files);for(let[r,i]of a)t.some(e=>i.test(e.data))||n.push(`required pattern "${r}" does not match any file of model answer "${e.id}" (comments are ignored)`);for(let[r,i]of o)for(let a of t){let t=a.data.match(i);t&&n.push(`forbidden pattern "${r}" matches model answer file ${e.id}/${a.path} (matched "${t[0]}")`)}for(let r of s)for(let i of t)i.data.includes(r)&&n.push(`forbidden text "${r}" appears in model answer file ${e.id}/${i.path}`)}}function U(e,t,n){for(let r of t){let t=new Set(r.files.map(e=>e.path));for(let i of e.requiredSubmissionFilePaths)t.has(i)||n.push(`required submission file "${i}" is missing from model answer "${r.id}" (requiredSubmissionFilePaths)`)}}function W(e){return e.flatMap(e=>{let t=h(e.path);if(!t||e.data.includes(`�`))return[];let r=t.grammer;return[{path:e.path,data:r?n(r,e.data):e.data}]})}function G(e,t,n){let i=[];for(let a of e){let{pattern:e}=r(a);try{i.push([e,new RegExp(e)])}catch{n.push(`invalid regular expression in ${t}: ${e}`)}}return i}async function K(e,t,n,r,i){let o=b(e,`templates`);if(!await s(o))return;let c=(await w(o,{withFileTypes:!0})).filter(e=>e.name!==`__pycache__`);c.some(e=>e.name===`.DS_Store`)&&i.push(`templates/.DS_Store found; delete it (the judge treats it as a template file)`);let l=c.some(e=>e.isFile()),u=c.some(e=>e.isDirectory());if(l&&u){r.push(`templates directory must not contain both files and directories; use templates/<languageId>/ or templates/_default/ (or plain files as the default)`);return}for(let e of c)e.isDirectory()&&e.name!==`_default`&&!a.includes(e.name)&&r.push(`invalid template directory name "${e.name}"; expected a language ID (${a.join(`, `)}) or _default`);let f=c.filter(e=>e.isDirectory()).map(e=>e.name),p=u?f:[``];for(let e of p){let r=await d(b(o,e)),a=new Map(r.map(e=>[e.path,e.data]));for(let r of t){let t=r.files.filter(e=>n||e.data.trim().length>0&&h(e.path)!==void 0);t.length>0&&t.every(e=>a.get(e.path)===e.data)&&i.push(`templates/${e} contains every judged file of model answer "${r.id}" unchanged (identical to a model answer file); templates must not pass the judge`)}}}export{j as validateProblemDirectory};
2
2
  //# sourceMappingURL=validateProblem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateProblem.js","names":[],"sources":["../../src/learningMaterial/validateProblem.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join, relative, resolve } from 'node:path';\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: ${error instanceof Error ? error.message : String(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":"ioCA6BA,MAIM,EAA0B,CAAC,UAAW,MAAO,MAAO,OAAO,EAC3D,EAAwB,CAC5B,mCACA,4CACA,6CACF,EAiBM,EAAyB,UAGzB,EAAqD,IAAI,IAAI,CAAC,YAAa,aAAa,CAAC,EAW/F,eAAsB,EAAyB,EAAyD,CACtG,IAAM,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACtB,EAAS,CAAE,SAAQ,UAAS,EAE5B,EAAwB,EAAQ,CAAoB,EAE1D,GAAI,CAAE,MAAM,EAAmB,CAAqB,EAMlD,OALA,EAAO,KACJ,MAAM,EAAY,CAAqB,EACpC,4EAA4E,IAC5E,gCAAgC,GACtC,EACO,EAGT,IAAM,EAAY,EAAS,CAAqB,EAC3C,EAA2B,KAAK,CAAS,GAC5C,EAAO,KAAK,eAAe,EAAU,gCAAgC,GAA4B,EAMnG,IAAK,IAAM,KAAY,MAAM,EAAkB,CAAqB,EAClE,EAAO,KACL,GAAG,EAAS,EAAuB,CAAQ,EAAE,8EAC/C,EAEF,IAAM,EAAiB,MAAM,EAAQ,EAAuB,CAAE,cAAe,EAAK,CAAC,EACnF,IAAK,IAAM,KAAU,EAAe,UAAU,EAAI,IAAO,EAAG,KAAK,cAAc,EAAG,IAAI,CAAC,EACjF,EAAO,OAAO,GAAK,EAAO,KAAK,SAAS,aAAa,GACvD,EAAO,KAAK,yBAAyB,EAAO,KAAK,yDAAyD,EAI9G,IAAM,EAAkB,EAAK,EAAuB,YAAY,EAEhE,GAAI,CAAE,MAAM,EAAc,CAAe,EAIvC,OAHM,MAAM,EAAO,CAAe,GAChC,EAAO,KAAK,sEAAsE,EAE7E,EAGT,IAAM,EAAc,MAAM,EAAwB,EAAiB,CAAM,EACnE,EAAU,MAAM,EAAiB,CAAqB,EACtD,EAAyB,IAAgB,IAAA,IAAa,CAAC,EAAmC,CAAW,EAS3G,EAAyB,EAAa,MARV,EAC1B,EACA,CAAE,iBAAkB,EAAQ,iBAAkB,wBAAuB,EACrE,EACA,CACF,EAGqD,EAAQ,iBAAkB,EAAQ,CAAQ,EAI/F,IAAM,EACJ,IAAgB,IAAA,IAAa,EAAY,0BAA4B,IAAQ,EAAY,OAAS,eACpG,EAAqB,EAAS,IAAgB,IAAA,GAAW,EAAQ,CAAQ,EAEzE,IAAM,EAAe,MAAM,EAAiB,EAAuB,EAAQ,CAAQ,EAI7E,EAAqB,EAAa,OAAQ,GAAW,CAEzD,GAAI,EAAQ,iBAAkB,MAAO,GACrC,IAAM,EAAa,EAAO,MAAM,KAC7B,GAAS,EAA6B,EAAK,IAAI,IAAM,IAAA,IAAa,EAAK,KAAK,KAAK,CAAC,CAAC,OAAS,CAC/F,EAMA,OALK,GACH,EAAO,KACL,2BAA2B,EAAO,GAAG,uGACvC,EAEK,CACT,CAAC,EAUD,OATI,GAAyB,EAAa,SAAW,GACnD,EAAO,KAAK,qGAAqG,EAE/G,IACF,EAAqB,EAAa,EAAoB,EAAQ,CAAQ,EACtE,EAAgC,EAAa,EAAoB,CAAM,GAEzE,MAAM,EAAkB,EAAuB,EAAoB,EAAQ,iBAAkB,EAAQ,CAAQ,EAEtG,CACT,CAQA,eAAe,EAAiB,EAAsD,CACpF,IAAM,EAAa,MAAM,EAAO,EAAK,EAAuB,UAAU,CAAC,EAGjE,EAA0B,MAAM,EAA6B,CAAqB,EACxF,MAAO,CACL,WAAY,MAAM,EAAO,EAAK,EAAuB,UAAU,CAAC,EAChE,gCAAiC,EACjC,iBAAkB,GAAc,CAAC,EAAwB,SAAS,UAAU,CAC9E,CACF,CAQA,SAAS,EACP,EACA,EACA,EACA,EACM,CACF,EAAQ,gCAAgC,SAAS,UAAU,GAC7D,EAAO,KACL,oMACF,EAEE,EAAQ,gCAAgC,SAAS,UAAU,GAC7D,EAAO,KACL,6GACF,EAOE,GAAwB,EAAQ,kBAAoB,CAAC,EAAQ,YAC/D,EAAS,KACP,2MACF,CAEJ,CAEA,eAAe,EACb,EACA,EACyC,CACzC,IAAI,EACJ,GAAI,CACF,IAAI,GACH,sBAAuB,EAAiB,MAAM,EAAS,EAAiB,MAAM,CAAC,GAEhF,EAAwB,EAAM,CAAM,CACtC,OAAS,EAAO,CACd,EAAO,KAAK,yCAAyC,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,EAC7G,MACF,CACA,IAAM,EAAS,EAAyB,UAAU,CAAU,EAC5D,GAAI,CAAC,EAAO,QAAS,CACnB,EAAO,KAAK,GAAG,EAAgB,EAAO,MAAO,wBAAwB,CAAC,EACtE,MACF,CACA,OAAO,EAAO,IAChB,CAEA,eAAe,EACb,EACA,CAAE,mBAAkB,0BACpB,EACA,EACyB,CACzB,IAAM,EAAyB,EAAK,EAAsB,YAAY,EACtE,GAAI,CAAE,MAAM,EAAY,CAAsB,EAAI,MAAO,CAAC,EAE1D,IAAM,EAAU,MAAM,EAAQ,EAAwB,CAAE,cAAe,EAAK,CAAC,EACvE,EAAe,IAAI,IACnB,EAAe,GAAqC,CACxD,IAAI,EAAW,EAAa,IAAI,CAAU,EAU1C,OATK,IACH,EAAW,CACT,GAAI,EACJ,aAAc,GACd,cAAe,GACf,eAAgB,EAClB,EACA,EAAa,IAAI,EAAY,CAAQ,GAEhC,CACT,EACA,IAAK,IAAM,KAAU,EAAQ,UAAU,EAAI,IAAO,EAAG,KAAK,cAAc,EAAG,IAAI,CAAC,EAAG,CAEjF,GAAI,EAAO,eAAe,EAAG,SAE7B,GAAI,EAAO,OAAS,GAA0B,EAAO,KAAK,WAAW,GAAG,EAAuB,EAAE,EAAG,CAC9F,EAAO,OAAS,GAAG,EAAuB,MAElC,EAAO,YAAY,GAIzB,MAD0B,EAAqB,EAAK,EAAwB,EAAO,IAAI,CAAC,EAAA,CACxE,SAAW,GAC7B,EAAO,KAAK,cAAc,EAAO,KAAK,qDAAqD,EAJ7F,EAAO,KAAK,cAAc,EAAO,KAAK,qBAAqB,EAF3D,EAAO,KAAK,cAAc,EAAO,KAAK,iEAAiE,EASzG,QACF,CACA,IAAM,EAAa,EAAO,KAAK,QAAQ,gBAAiB,EAAE,EAC1D,GAAI,IAAe,GAAI,CACrB,EAAO,KAAK,cAAc,EAAO,KAAK,+CAA+C,EACrF,QACF,CACA,GAAI,IAAe,EAAO,KAAM,CAE1B,EAAO,YAAY,GAAK,mBAAmB,KAAK,EAAO,IAAI,GAC7D,EAAO,KAAK,cAAc,EAAO,KAAK,iCAAiC,EAEzE,QACF,CACA,GAAI,CAAC,EAAO,YAAY,EAAG,CACzB,EAAO,KAAK,aAAa,EAAW,eAAe,EAAO,KAAK,qBAAqB,EACpF,QACF,CAIA,IAAM,EAAY,MAAM,EAAqB,EAAK,EAAwB,EAAO,IAAI,CAAC,EACtF,GAAI,EAAU,SAAW,EAAG,CAC1B,EAAO,KAAK,aAAa,EAAW,IAAI,EAAO,KAAK,qDAAqD,EACzG,QACF,CACA,IAAM,EAAW,EAAY,CAAU,EACvC,GAAI,EAAO,KAAK,SAAS,MAAM,EAAG,CAChC,EAAS,aAAe,GACxB,QACF,CACA,EAAS,cAAgB,GAEzB,IAAK,IAAM,KAAY,EACjB,EAAS,CAAQ,IAAM,YACzB,EAAS,KACP,aAAa,EAAW,IAAI,EAAO,KAAK,GAAG,EAAS,EAAK,EAAwB,EAAO,IAAI,EAAG,CAAQ,EAAE,gEAC3G,EAKJ,GAAI,CAAC,EACE,IAAA,IAAM,KAAY,GAEjB,MADoB,EAAK,CAAQ,EAAA,CACvB,KAAA,SACZ,EAAO,KACL,aAAa,EAAW,IAAI,EAAO,KAAK,GAAG,EAAS,EAAK,EAAwB,EAAO,IAAI,EAAG,CAAQ,EAAE,kBAAkB,EAAwB,6CACrJ,CAIR,CAEA,IAAM,EAAY,EACf,OACE,GACC,EAAO,OAAO,GAAK,mBAAmB,KAAK,EAAO,IAAI,GAAK,CAAC,EAAO,KAAK,WAAW,GAAG,EAAuB,EAAE,CACnH,CAAC,CACA,IAAK,GAAW,EAAO,IAAI,CAAC,CAC5B,UAAU,EAAI,IAAO,EAAG,cAAc,CAAE,CAAC,EAC5C,IAAK,IAAM,KAAY,EAAW,CAChC,IAAM,EAAa,EAAS,QAAQ,mBAAoB,EAAE,EAC1D,GAAI,IAAe,GAAI,CACrB,EAAO,KAAK,cAAc,EAAS,+CAA+C,EAClF,QACF,CACA,IAAM,EAAW,EAAY,CAAU,EACjC,EAAU,MAAM,EAAS,EAAK,EAAwB,CAAQ,EAAG,MAAM,EAC7E,GAAI,EAAS,SAAS,KAAK,EACzB,EAAS,MAAQ,EAAQ,QAAQ,OAC5B,GAAI,EAAS,SAAS,MAAM,EACjC,EAAS,OAAS,EAAQ,QAAQ,MAC7B,CAEL,EAAS,eAAiB,GACrB,GACH,EAAO,KACL,aAAa,EAAW,IAAI,EAAS,0EACvC,EAEF,GAAI,CACF,KAAK,MAAM,CAAO,CACpB,MAAQ,CACN,EAAO,KAAK,aAAa,EAAW,IAAI,EAAS,mBAAmB,CACtE,CACF,CACF,CAIA,IAAM,EAAY,CAAC,GAAG,EAAa,OAAO,CAAC,CAAC,CAAC,UAAU,EAAI,IAAO,EAAG,GAAG,cAAc,EAAG,EAAE,CAAC,EAKtF,EAAuB,EAAU,KAAK,CAAiB,EAC7D,IAAK,IAAM,KAAY,EAAW,CAIhC,GAAI,CAAC,EAAkB,CAAQ,GAAK,CAAC,EAAS,eAAgB,CAE5D,GAAK,EAKM,GACT,EAAS,KACP,aAAa,EAAS,GAAG,IAAI,EAAS,GAAG,WAAW,EAAS,GAAG,8CAClE,MARqB,CACrB,GAAI,CAAC,EAAwB,SAC7B,EAAO,KACL,aAAa,EAAS,GAAG,IAAI,EAAS,GAAG,WAAW,EAAS,GAAG,4FAClE,CACF,CAKF,CAOA,GAAI,EAAS,SAAW,IAAA,GAAW,CACjC,IAAM,EAAkB,EAAmB,IAA6B,EACpE,EAAS,OAAO,OAAS,GAC3B,EAAO,KACL,EACI,aAAa,EAAS,GAAG,+BAA+B,EAAS,OAAO,OAAO,KAAK,EAAgB,GACpG,aAAa,EAAS,GAAG,+BAA+B,EAAS,OAAO,OAAO,yDAAyD,EAAgB,YAC9J,CAEJ,CAGI,EAAS,SAAW,IAAA,IAAa,EAAS,OAAO,SAAW,GAC9D,EAAS,KACP,aAAa,EAAS,GAAG,+HAC3B,CAEJ,CAIA,IAAM,EAAiB,IAAI,IAC3B,IAAK,IAAM,KAAY,EAAW,CAEhC,GAAI,CAAC,EAAS,OAAS,EAAS,aAAc,SAC9C,IAAM,EAAU,EAAe,IAAI,EAAS,KAAK,EAC7C,IAAY,IAAA,GACd,EAAe,IAAI,EAAS,MAAO,EAAS,EAAE,EAE9C,EAAS,KAAK,cAAc,EAAQ,OAAO,EAAS,GAAG,sBAAsB,CAEjF,CACA,OAAO,CACT,CAEA,eAAe,EAAkB,EAA0C,CACzE,IAAM,EAAsB,CAAC,EAC7B,IAAK,IAAM,KAAU,MAAM,EAAQ,EAAe,CAAE,cAAe,EAAK,CAAC,EAAG,CAC1E,IAAM,EAAY,EAAK,EAAe,EAAO,IAAI,EAC7C,EAAO,eAAe,EACxB,EAAU,KAAK,CAAS,EACf,EAAO,YAAY,GAAK,EAAO,OAAS,gBACjD,EAAU,KAAK,GAAI,MAAM,EAAkB,CAAS,CAAE,CAE1D,CACA,OAAO,EAAU,UAAU,EAAI,IAAO,EAAG,cAAc,CAAE,CAAC,CAC5D,CAGA,eAAe,EAAqB,EAA0C,CAC5E,IAAM,EAAsB,CAAC,EAC7B,IAAK,IAAM,KAAU,MAAM,EAAQ,EAAe,CAAE,cAAe,EAAK,CAAC,EAAG,CAC1E,GAAI,EAA8B,IAAI,EAAO,IAAI,EAAG,SACpD,IAAM,EAAY,EAAK,EAAe,EAAO,IAAI,EAC7C,EAAO,OAAO,EAChB,EAAU,KAAK,CAAS,EACf,EAAO,YAAY,GAC5B,EAAU,KAAK,GAAI,MAAM,EAAqB,CAAS,CAAE,CAE7D,CACA,OAAO,CACT,CAEA,SAAS,EAAkB,EAAiC,CAC1D,OAAO,EAAS,SAAW,IAAA,IAAa,EAAS,aACnD,CAEA,SAAS,EACP,EACA,EACA,EACA,EACA,EACM,CAIF,EAAc,SAAW,GAAK,GAAa,0BAA4B,KAGrE,EACF,EAAS,KACP,sJACF,EAEA,EAAO,KACL,6OACF,GAGA,EAAc,OAAS,GACzB,EAAgC,EAAe,EAAQ,CAAQ,CAEnE,CAEA,SAAS,EACP,EACA,EACA,EACM,CACD,EAAU,KAAM,GAAa,EAA6B,KAAK,EAAS,EAAE,CAAC,GAC9E,EAAO,KAAK,qGAAqG,EAE/G,EAAU,MAAO,GAAa,EAA6B,KAAK,EAAS,EAAE,CAAC,GAC9E,EAAO,KACL,yGACF,EAEE,EAAU,OAAS,GACrB,EAAS,KACP,QAAQ,EAAU,OAAO,6CAC3B,CAEJ,CAEA,eAAe,EACb,EACA,EACA,EACwB,CACxB,IAAM,EAA4B,EAAK,EAAsB,eAAe,EAC5E,GAAI,CAAE,MAAM,EAAY,CAAyB,EAAI,MAAO,CAAC,EAE7D,IAAM,EAA8B,CAAC,EAC/B,EAAU,MAAM,EAAQ,EAA2B,CAAE,cAAe,EAAK,CAAC,EAChF,IAAK,IAAM,KAAU,EAAQ,UAAU,EAAI,IAAO,EAAG,KAAK,cAAc,EAAG,IAAI,CAAC,EAAG,CACjF,GAAI,CAAC,EAAO,YAAY,GAAK,EAAO,OAAS,cAAe,SAG5D,GAAI,CAAC,EAA2B,KAAK,EAAO,IAAI,EAAG,CACjD,EAAO,KAAK,gCAAgC,EAAO,KAAK,eAAe,GAA4B,EACnG,QACF,CACM,EAA2C,SAAS,EAAO,IAAI,GACnE,EAAS,KACP,gCAAgC,EAAO,KAAK,gCAAgC,EAAqB,KAAK,IAAI,EAAE,EAC9G,EAEF,IAAM,EAAQ,MAAM,EAA2B,EAAK,EAA2B,EAAO,IAAI,CAAC,EAE3F,GAAI,EAAM,SAAW,EAAG,CACtB,EAAO,KAAK,2BAA2B,EAAO,KAAK,8CAA8C,EACjG,QACF,CACA,EAAa,KAAK,CAAE,GAAI,EAAO,KAAM,OAAM,CAAC,CAC9C,CACA,OAAO,CACT,CAEA,SAAS,EACP,EACA,EACA,EACA,EACM,CACN,IAAM,EAAkB,EAAgB,EAAY,sBAAuB,wBAAyB,CAAM,EACpG,EAAmB,EAAgB,EAAY,uBAAwB,yBAA0B,CAAM,EACvG,EAAiB,EAAY,qBAAqB,IAAK,GAAS,EAAkB,CAAI,CAAC,CAAC,OAAO,EAErG,IAAK,GAAM,CAAC,EAAS,KAAW,EAC1B,EAAsB,KAAM,GAAY,EAAO,KAAK,CAAO,CAAC,GAC9D,EAAS,KACP,mCAAmC,EAAQ,oFAAoF,EAAwB,KAAK,IAAI,EAAE,EACpK,EAGJ,IAAK,IAAM,KAAQ,EACb,EAAsB,KAAM,GAAY,EAAQ,SAAS,CAAI,CAAC,GAChE,EAAS,KACP,8BAA8B,EAAK,oFAAoF,EAAwB,KAAK,IAAI,EAAE,EAC5J,EAIJ,IAAK,IAAM,KAAe,EAAc,CACtC,IAAM,EAAgB,EAA8B,EAAY,KAAK,EAErE,IAAK,GAAM,CAAC,EAAS,KAAW,EACzB,EAAc,KAAM,GAAS,EAAO,KAAK,EAAK,IAAI,CAAC,GACtD,EAAO,KACL,qBAAqB,EAAQ,6CAA6C,EAAY,GAAG,yBAC3F,EAGJ,IAAK,GAAM,CAAC,EAAS,KAAW,EAC9B,IAAK,IAAM,KAAQ,EAAe,CAChC,IAAM,EAAU,EAAK,KAAK,MAAM,CAAM,EAClC,GACF,EAAO,KACL,sBAAsB,EAAQ,8BAA8B,EAAY,GAAG,GAAG,EAAK,KAAK,aAAa,EAAQ,GAAG,GAClH,CAEJ,CAEF,IAAK,IAAM,KAAiB,EAC1B,IAAK,IAAM,KAAQ,EACb,EAAK,KAAK,SAAS,CAAa,GAClC,EAAO,KAAK,mBAAmB,EAAc,iCAAiC,EAAY,GAAG,GAAG,EAAK,MAAM,CAInH,CACF,CAMA,SAAS,EACP,EACA,EACA,EACM,CACN,IAAK,IAAM,KAAe,EAAc,CACtC,IAAM,EAAc,IAAI,IAAI,EAAY,MAAM,IAAK,GAAS,EAAK,IAAI,CAAC,EACtE,IAAK,IAAM,KAAgB,EAAY,4BAChC,EAAY,IAAI,CAAY,GAC/B,EAAO,KACL,6BAA6B,EAAa,kCAAkC,EAAY,GAAG,gCAC7F,CAGN,CACF,CAGA,SAAS,EAA8B,EAAmC,CACxE,OAAO,EAAM,QAAS,GAAS,CAC7B,IAAM,EAAqB,EAA6B,EAAK,IAAI,EACjE,GAAI,CAAC,GAAsB,EAAK,KAAK,SAAS,GAAQ,EAAG,MAAO,CAAC,EACjE,IAAM,EAAU,EAAmB,QACnC,MAAO,CAAC,CAAE,KAAM,EAAK,KAAM,KAAM,EAAU,EAA2B,EAAS,EAAK,IAAI,EAAI,EAAK,IAAK,CAAC,CACzG,CAAC,CACH,CAEA,SAAS,EAAgB,EAAmB,EAAmB,EAAsC,CACnG,IAAM,EAA+B,CAAC,EACtC,IAAK,IAAM,KAAQ,EAAO,CACxB,GAAM,CAAE,WAAY,EAAkB,CAAI,EAC1C,GAAI,CACF,EAAS,KAAK,CAAC,EAAS,IAAI,OAAO,CAAO,CAAC,CAAC,CAC9C,MAAQ,CACN,EAAO,KAAK,iCAAiC,EAAU,IAAI,GAAS,CACtE,CACF,CACA,OAAO,CACT,CAEA,eAAe,EACb,EACA,EACA,EACA,EACA,EACe,CACf,IAAM,EAAyB,EAAK,EAAsB,WAAW,EACrE,GAAI,CAAE,MAAM,EAAY,CAAsB,EAAI,OAKlD,IAAM,GAAU,MAHM,EAAQ,EAAwB,CAAE,cAAe,EAAK,CAAC,EAAA,CAGrD,OAAQ,GAAW,EAAO,OAAS,aAAa,EACpE,EAAQ,KAAM,GAAW,EAAO,OAAS,WAAW,GACtD,EAAS,KAAK,+EAA+E,EAE/F,IAAM,EAAW,EAAQ,KAAM,GAAW,EAAO,OAAO,CAAC,EACnD,EAAiB,EAAQ,KAAM,GAAW,EAAO,YAAY,CAAC,EACpE,GAAI,GAAY,EAAgB,CAC9B,EAAO,KACL,qJACF,EACA,MACF,CACA,IAAK,IAAM,KAAU,EAEjB,EAAO,YAAY,GACnB,EAAO,OAAS,YAChB,CAAE,EAA2C,SAAS,EAAO,IAAI,GAEjE,EAAO,KACL,oCAAoC,EAAO,KAAK,6BAA6B,EAAqB,KAAK,IAAI,EAAE,cAC/G,EAOJ,IAAM,EAAiB,EAAQ,OAAQ,GAAW,EAAO,YAAY,CAAC,CAAC,CAAC,IAAK,GAAW,EAAO,IAAI,EAC7F,EAAyB,EAAiB,EAAiB,CAAC,EAAE,EACpE,IAAK,IAAM,KAAyB,EAAwB,CAC1D,IAAM,EAAgB,MAAM,EAA2B,EAAK,EAAwB,CAAqB,CAAC,EACpG,EAAwB,IAAI,IAAI,EAAc,IAAK,GAAS,CAAC,EAAK,KAAM,EAAK,IAAI,CAAC,CAAC,EACzF,IAAK,IAAM,KAAe,EAAc,CACtC,IAAM,EAAc,EAAY,MAAM,OACnC,GACC,GAAqB,EAAK,KAAK,KAAK,CAAC,CAAC,OAAS,GAAK,EAA6B,EAAK,IAAI,IAAM,IAAA,EACpG,EACI,EAAY,OAAS,GAAK,EAAY,MAAO,GAAS,EAAsB,IAAI,EAAK,IAAI,IAAM,EAAK,IAAI,GAC1G,EAAS,KACP,aAAa,EAAsB,+CAA+C,EAAY,GAAG,kFACnG,CAEJ,CACF,CACF"}
1
+ {"version":3,"file":"validateProblem.js","names":[],"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,3 +1,3 @@
1
1
  "use strict";function e(e,n){return`${[n.errors.length===0?`${e}: OK${n.warnings.length>0?` (${t(n.warnings.length,`warning`)})`:``}`:`${e}: NG (${[t(n.errors.length,`error`),n.warnings.length>0?t(n.warnings.length,`warning`):``].filter(e=>e.length>0).join(`, `)})`,...n.errors.map(e=>` error: ${e}`),...n.warnings.map(e=>` warning: ${e}`)].join(`
2
- `)}\n`}function t(e,t){return`${e} ${t}${e===1?``:`s`}`}function n(e,t){return e.issues.map(e=>`${t}: ${e.path.length>0?`${e.path.join(`.`)}: `:``}${e.message}${r(e)}`)}function r(e){if(e.code!==`invalid_union`)return``;let t=[...new Set(e.errors.flat().map(e=>e.message))];return t.length>0?` (${t.join(`; `)})`:``}function i(e,t,n){let r=new Set,i=new Set;for(let t of e)(r.has(t)?i:r).add(t);i.size>0&&n.push(`duplicate ${t} IDs: ${[...i].join(`, `)}`)}exports.formatValidationReport=e,exports.formatZodIssues=n,exports.reportDuplicateIds=i;
2
+ `)}\n`}function t(e,t){return`${e} ${t}${e===1?``:`s`}`}function n(e,t){return e.issues.map(e=>`${t}: ${e.path.length>0?`${e.path.join(`.`)}: `:``}${e.message}${r(e)}`)}function r(e){if(e.code!==`invalid_union`)return``;let t=[...new Set(e.errors.flat().map(e=>e.message))];return t.length>0?` (${t.join(`; `)})`:``}function i(e,t,n){let r=/* @__PURE__ */ new Set,i=/* @__PURE__ */ new Set;for(let t of e)(r.has(t)?i:r).add(t);i.size>0&&n.push(`duplicate ${t} IDs: ${[...i].join(`, `)}`)}exports.formatValidationReport=e,exports.formatZodIssues=n,exports.reportDuplicateIds=i;
3
3
  //# sourceMappingURL=validationResult.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validationResult.cjs","names":[],"sources":["../../src/learningMaterial/validationResult.ts"],"sourcesContent":["import type { z } from 'zod';\n\n/** Outcome of a deterministic learning-material validation; errors are fatal, warnings are advisory. */\nexport interface ValidationResult {\n errors: string[];\n warnings: string[];\n}\n\nexport function formatValidationReport(label: string, result: ValidationResult): string {\n const headline =\n result.errors.length === 0\n ? `${label}: OK${result.warnings.length > 0 ? ` (${countText(result.warnings.length, 'warning')})` : ''}`\n : `${label}: NG (${[\n countText(result.errors.length, 'error'),\n result.warnings.length > 0 ? countText(result.warnings.length, 'warning') : '',\n ]\n .filter((text) => text.length > 0)\n .join(', ')})`;\n const lines = [\n headline,\n ...result.errors.map((error) => ` error: ${error}`),\n ...result.warnings.map((warning) => ` warning: ${warning}`),\n ];\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction countText(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n\nexport function formatZodIssues(error: z.ZodError, prefix: string): string[] {\n return error.issues.map(\n (issue) =>\n `${prefix}: ${issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''}${issue.message}${describeUnionBranches(issue)}`\n );\n}\n\n// A union reports only \"Invalid input\", hiding the branch messages that say what was expected.\nfunction describeUnionBranches(issue: z.core.$ZodIssue): string {\n if (issue.code !== 'invalid_union') return '';\n const branchMessages = [...new Set(issue.errors.flat().map((branchIssue) => branchIssue.message))];\n return branchMessages.length > 0 ? ` (${branchMessages.join('; ')})` : '';\n}\n\nexport function reportDuplicateIds(ids: string[], itemType: string, errors: string[]): void {\n const seenIds = new Set<string>();\n const duplicateIds = new Set<string>();\n for (const id of ids) {\n (seenIds.has(id) ? duplicateIds : seenIds).add(id);\n }\n if (duplicateIds.size > 0) {\n errors.push(`duplicate ${itemType} IDs: ${[...duplicateIds].join(', ')}`);\n }\n}\n"],"mappings":"aAQA,SAAgB,EAAuB,EAAe,EAAkC,CAetF,MAAO,GAAG,CAbR,EAAO,OAAO,SAAW,EACrB,GAAG,EAAM,MAAM,EAAO,SAAS,OAAS,EAAI,KAAK,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAE,GAAK,KACnG,GAAG,EAAM,QAAQ,CACf,EAAU,EAAO,OAAO,OAAQ,OAAO,EACvC,EAAO,SAAS,OAAS,EAAI,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAI,EAC9E,CAAC,CACE,OAAQ,GAAS,EAAK,OAAS,CAAC,CAAC,CACjC,KAAK,IAAI,EAAE,GAGlB,GAAG,EAAO,OAAO,IAAK,GAAU,YAAY,GAAO,EACnD,GAAG,EAAO,SAAS,IAAK,GAAY,cAAc,GAAS,CAE/C,CAAC,CAAC,KAAK;CAAI,EAAE,GAC7B,CAEA,SAAS,EAAU,EAAe,EAAsB,CACtD,MAAO,GAAG,EAAM,GAAG,IAAO,IAAU,EAAI,GAAK,KAC/C,CAEA,SAAgB,EAAgB,EAAmB,EAA0B,CAC3E,OAAO,EAAM,OAAO,IACjB,GACC,GAAG,EAAO,IAAI,EAAM,KAAK,OAAS,EAAI,GAAG,EAAM,KAAK,KAAK,GAAG,EAAE,IAAM,KAAK,EAAM,UAAU,EAAsB,CAAK,GACxH,CACF,CAGA,SAAS,EAAsB,EAAiC,CAC9D,GAAI,EAAM,OAAS,gBAAiB,MAAO,GAC3C,IAAM,EAAiB,CAAC,GAAG,IAAI,IAAI,EAAM,OAAO,KAAK,CAAC,CAAC,IAAK,GAAgB,EAAY,OAAO,CAAC,CAAC,EACjG,OAAO,EAAe,OAAS,EAAI,KAAK,EAAe,KAAK,IAAI,EAAE,GAAK,EACzE,CAEA,SAAgB,EAAmB,EAAe,EAAkB,EAAwB,CAC1F,IAAM,EAAU,IAAI,IACd,EAAe,IAAI,IACzB,IAAK,IAAM,KAAM,GACd,EAAQ,IAAI,CAAE,EAAI,EAAe,EAAA,CAAS,IAAI,CAAE,EAE/C,EAAa,KAAO,GACtB,EAAO,KAAK,aAAa,EAAS,QAAQ,CAAC,GAAG,CAAY,CAAC,CAAC,KAAK,IAAI,GAAG,CAE5E"}
1
+ {"version":3,"file":"validationResult.cjs","names":[],"sources":["../../src/learningMaterial/validationResult.ts"],"sourcesContent":["import type { z } from 'zod';\n\n/** Outcome of a deterministic learning-material validation; errors are fatal, warnings are advisory. */\nexport interface ValidationResult {\n errors: string[];\n warnings: string[];\n}\n\nexport function formatValidationReport(label: string, result: ValidationResult): string {\n const headline =\n result.errors.length === 0\n ? `${label}: OK${result.warnings.length > 0 ? ` (${countText(result.warnings.length, 'warning')})` : ''}`\n : `${label}: NG (${[\n countText(result.errors.length, 'error'),\n result.warnings.length > 0 ? countText(result.warnings.length, 'warning') : '',\n ]\n .filter((text) => text.length > 0)\n .join(', ')})`;\n const lines = [\n headline,\n ...result.errors.map((error) => ` error: ${error}`),\n ...result.warnings.map((warning) => ` warning: ${warning}`),\n ];\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction countText(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n\nexport function formatZodIssues(error: z.ZodError, prefix: string): string[] {\n return error.issues.map(\n (issue) =>\n `${prefix}: ${issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''}${issue.message}${describeUnionBranches(issue)}`\n );\n}\n\n// A union reports only \"Invalid input\", hiding the branch messages that say what was expected.\nfunction describeUnionBranches(issue: z.core.$ZodIssue): string {\n if (issue.code !== 'invalid_union') return '';\n const branchMessages = [...new Set(issue.errors.flat().map((branchIssue) => branchIssue.message))];\n return branchMessages.length > 0 ? ` (${branchMessages.join('; ')})` : '';\n}\n\nexport function reportDuplicateIds(ids: string[], itemType: string, errors: string[]): void {\n const seenIds = new Set<string>();\n const duplicateIds = new Set<string>();\n for (const id of ids) {\n (seenIds.has(id) ? duplicateIds : seenIds).add(id);\n }\n if (duplicateIds.size > 0) {\n errors.push(`duplicate ${itemType} IDs: ${[...duplicateIds].join(', ')}`);\n }\n}\n"],"mappings":"aAQA,SAAgB,EAAuB,EAAe,EAAkC,CAetF,MAAO,GAAG,CAbR,EAAO,OAAO,SAAW,EACrB,GAAG,EAAM,MAAM,EAAO,SAAS,OAAS,EAAI,KAAK,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAE,GAAK,KACnG,GAAG,EAAM,QAAQ,CACf,EAAU,EAAO,OAAO,OAAQ,OAAO,EACvC,EAAO,SAAS,OAAS,EAAI,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAI,EAC9E,CAAC,CACE,OAAQ,GAAS,EAAK,OAAS,CAAC,CAAC,CACjC,KAAK,IAAI,EAAE,GAGlB,GAAG,EAAO,OAAO,IAAK,GAAU,YAAY,GAAO,EACnD,GAAG,EAAO,SAAS,IAAK,GAAY,cAAc,GAAS,CAE/C,CAAC,CAAC,KAAK;CAAI,EAAE,GAC7B,CAEA,SAAS,EAAU,EAAe,EAAsB,CACtD,MAAO,GAAG,EAAM,GAAG,IAAO,IAAU,EAAI,GAAK,KAC/C,CAEA,SAAgB,EAAgB,EAAmB,EAA0B,CAC3E,OAAO,EAAM,OAAO,IACjB,GACC,GAAG,EAAO,IAAI,EAAM,KAAK,OAAS,EAAI,GAAG,EAAM,KAAK,KAAK,GAAG,EAAE,IAAM,KAAK,EAAM,UAAU,EAAsB,CAAK,GACxH,CACF,CAGA,SAAS,EAAsB,EAAiC,CAC9D,GAAI,EAAM,OAAS,gBAAiB,MAAO,GAC3C,IAAM,EAAiB,CAAC,GAAG,IAAI,IAAI,EAAM,OAAO,KAAK,CAAC,CAAC,IAAK,GAAgB,EAAY,OAAO,CAAC,CAAC,EACjG,OAAO,EAAe,OAAS,EAAI,KAAK,EAAe,KAAK,IAAI,EAAE,GAAK,EACzE,CAEA,SAAgB,EAAmB,EAAe,EAAkB,EAAwB,CAC1F,IAAM,kBAAU,IAAI,IACd,kBAAe,IAAI,IACzB,IAAK,IAAM,KAAM,GACd,EAAQ,IAAI,CAAE,EAAI,EAAe,EAAA,CAAS,IAAI,CAAE,EAE/C,EAAa,KAAO,GACtB,EAAO,KAAK,aAAa,EAAS,QAAQ,CAAC,GAAG,CAAY,CAAC,CAAC,KAAK,IAAI,GAAG,CAE5E"}
@@ -1,3 +1,3 @@
1
1
  function e(e,n){return`${[n.errors.length===0?`${e}: OK${n.warnings.length>0?` (${t(n.warnings.length,`warning`)})`:``}`:`${e}: NG (${[t(n.errors.length,`error`),n.warnings.length>0?t(n.warnings.length,`warning`):``].filter(e=>e.length>0).join(`, `)})`,...n.errors.map(e=>` error: ${e}`),...n.warnings.map(e=>` warning: ${e}`)].join(`
2
- `)}\n`}function t(e,t){return`${e} ${t}${e===1?``:`s`}`}function n(e,t){return e.issues.map(e=>`${t}: ${e.path.length>0?`${e.path.join(`.`)}: `:``}${e.message}${r(e)}`)}function r(e){if(e.code!==`invalid_union`)return``;let t=[...new Set(e.errors.flat().map(e=>e.message))];return t.length>0?` (${t.join(`; `)})`:``}function i(e,t,n){let r=new Set,i=new Set;for(let t of e)(r.has(t)?i:r).add(t);i.size>0&&n.push(`duplicate ${t} IDs: ${[...i].join(`, `)}`)}export{e as formatValidationReport,n as formatZodIssues,i as reportDuplicateIds};
2
+ `)}\n`}function t(e,t){return`${e} ${t}${e===1?``:`s`}`}function n(e,t){return e.issues.map(e=>`${t}: ${e.path.length>0?`${e.path.join(`.`)}: `:``}${e.message}${r(e)}`)}function r(e){if(e.code!==`invalid_union`)return``;let t=[...new Set(e.errors.flat().map(e=>e.message))];return t.length>0?` (${t.join(`; `)})`:``}function i(e,t,n){let r=/* @__PURE__ */ new Set,i=/* @__PURE__ */ new Set;for(let t of e)(r.has(t)?i:r).add(t);i.size>0&&n.push(`duplicate ${t} IDs: ${[...i].join(`, `)}`)}export{e as formatValidationReport,n as formatZodIssues,i as reportDuplicateIds};
3
3
  //# sourceMappingURL=validationResult.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validationResult.js","names":[],"sources":["../../src/learningMaterial/validationResult.ts"],"sourcesContent":["import type { z } from 'zod';\n\n/** Outcome of a deterministic learning-material validation; errors are fatal, warnings are advisory. */\nexport interface ValidationResult {\n errors: string[];\n warnings: string[];\n}\n\nexport function formatValidationReport(label: string, result: ValidationResult): string {\n const headline =\n result.errors.length === 0\n ? `${label}: OK${result.warnings.length > 0 ? ` (${countText(result.warnings.length, 'warning')})` : ''}`\n : `${label}: NG (${[\n countText(result.errors.length, 'error'),\n result.warnings.length > 0 ? countText(result.warnings.length, 'warning') : '',\n ]\n .filter((text) => text.length > 0)\n .join(', ')})`;\n const lines = [\n headline,\n ...result.errors.map((error) => ` error: ${error}`),\n ...result.warnings.map((warning) => ` warning: ${warning}`),\n ];\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction countText(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n\nexport function formatZodIssues(error: z.ZodError, prefix: string): string[] {\n return error.issues.map(\n (issue) =>\n `${prefix}: ${issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''}${issue.message}${describeUnionBranches(issue)}`\n );\n}\n\n// A union reports only \"Invalid input\", hiding the branch messages that say what was expected.\nfunction describeUnionBranches(issue: z.core.$ZodIssue): string {\n if (issue.code !== 'invalid_union') return '';\n const branchMessages = [...new Set(issue.errors.flat().map((branchIssue) => branchIssue.message))];\n return branchMessages.length > 0 ? ` (${branchMessages.join('; ')})` : '';\n}\n\nexport function reportDuplicateIds(ids: string[], itemType: string, errors: string[]): void {\n const seenIds = new Set<string>();\n const duplicateIds = new Set<string>();\n for (const id of ids) {\n (seenIds.has(id) ? duplicateIds : seenIds).add(id);\n }\n if (duplicateIds.size > 0) {\n errors.push(`duplicate ${itemType} IDs: ${[...duplicateIds].join(', ')}`);\n }\n}\n"],"mappings":"AAQA,SAAgB,EAAuB,EAAe,EAAkC,CAetF,MAAO,GAAG,CAbR,EAAO,OAAO,SAAW,EACrB,GAAG,EAAM,MAAM,EAAO,SAAS,OAAS,EAAI,KAAK,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAE,GAAK,KACnG,GAAG,EAAM,QAAQ,CACf,EAAU,EAAO,OAAO,OAAQ,OAAO,EACvC,EAAO,SAAS,OAAS,EAAI,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAI,EAC9E,CAAC,CACE,OAAQ,GAAS,EAAK,OAAS,CAAC,CAAC,CACjC,KAAK,IAAI,EAAE,GAGlB,GAAG,EAAO,OAAO,IAAK,GAAU,YAAY,GAAO,EACnD,GAAG,EAAO,SAAS,IAAK,GAAY,cAAc,GAAS,CAE/C,CAAC,CAAC,KAAK;CAAI,EAAE,GAC7B,CAEA,SAAS,EAAU,EAAe,EAAsB,CACtD,MAAO,GAAG,EAAM,GAAG,IAAO,IAAU,EAAI,GAAK,KAC/C,CAEA,SAAgB,EAAgB,EAAmB,EAA0B,CAC3E,OAAO,EAAM,OAAO,IACjB,GACC,GAAG,EAAO,IAAI,EAAM,KAAK,OAAS,EAAI,GAAG,EAAM,KAAK,KAAK,GAAG,EAAE,IAAM,KAAK,EAAM,UAAU,EAAsB,CAAK,GACxH,CACF,CAGA,SAAS,EAAsB,EAAiC,CAC9D,GAAI,EAAM,OAAS,gBAAiB,MAAO,GAC3C,IAAM,EAAiB,CAAC,GAAG,IAAI,IAAI,EAAM,OAAO,KAAK,CAAC,CAAC,IAAK,GAAgB,EAAY,OAAO,CAAC,CAAC,EACjG,OAAO,EAAe,OAAS,EAAI,KAAK,EAAe,KAAK,IAAI,EAAE,GAAK,EACzE,CAEA,SAAgB,EAAmB,EAAe,EAAkB,EAAwB,CAC1F,IAAM,EAAU,IAAI,IACd,EAAe,IAAI,IACzB,IAAK,IAAM,KAAM,GACd,EAAQ,IAAI,CAAE,EAAI,EAAe,EAAA,CAAS,IAAI,CAAE,EAE/C,EAAa,KAAO,GACtB,EAAO,KAAK,aAAa,EAAS,QAAQ,CAAC,GAAG,CAAY,CAAC,CAAC,KAAK,IAAI,GAAG,CAE5E"}
1
+ {"version":3,"file":"validationResult.js","names":[],"sources":["../../src/learningMaterial/validationResult.ts"],"sourcesContent":["import type { z } from 'zod';\n\n/** Outcome of a deterministic learning-material validation; errors are fatal, warnings are advisory. */\nexport interface ValidationResult {\n errors: string[];\n warnings: string[];\n}\n\nexport function formatValidationReport(label: string, result: ValidationResult): string {\n const headline =\n result.errors.length === 0\n ? `${label}: OK${result.warnings.length > 0 ? ` (${countText(result.warnings.length, 'warning')})` : ''}`\n : `${label}: NG (${[\n countText(result.errors.length, 'error'),\n result.warnings.length > 0 ? countText(result.warnings.length, 'warning') : '',\n ]\n .filter((text) => text.length > 0)\n .join(', ')})`;\n const lines = [\n headline,\n ...result.errors.map((error) => ` error: ${error}`),\n ...result.warnings.map((warning) => ` warning: ${warning}`),\n ];\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction countText(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n\nexport function formatZodIssues(error: z.ZodError, prefix: string): string[] {\n return error.issues.map(\n (issue) =>\n `${prefix}: ${issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''}${issue.message}${describeUnionBranches(issue)}`\n );\n}\n\n// A union reports only \"Invalid input\", hiding the branch messages that say what was expected.\nfunction describeUnionBranches(issue: z.core.$ZodIssue): string {\n if (issue.code !== 'invalid_union') return '';\n const branchMessages = [...new Set(issue.errors.flat().map((branchIssue) => branchIssue.message))];\n return branchMessages.length > 0 ? ` (${branchMessages.join('; ')})` : '';\n}\n\nexport function reportDuplicateIds(ids: string[], itemType: string, errors: string[]): void {\n const seenIds = new Set<string>();\n const duplicateIds = new Set<string>();\n for (const id of ids) {\n (seenIds.has(id) ? duplicateIds : seenIds).add(id);\n }\n if (duplicateIds.size > 0) {\n errors.push(`duplicate ${itemType} IDs: ${[...duplicateIds].join(', ')}`);\n }\n}\n"],"mappings":"AAQA,SAAgB,EAAuB,EAAe,EAAkC,CAetF,MAAO,GAAG,CAbR,EAAO,OAAO,SAAW,EACrB,GAAG,EAAM,MAAM,EAAO,SAAS,OAAS,EAAI,KAAK,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAE,GAAK,KACnG,GAAG,EAAM,QAAQ,CACf,EAAU,EAAO,OAAO,OAAQ,OAAO,EACvC,EAAO,SAAS,OAAS,EAAI,EAAU,EAAO,SAAS,OAAQ,SAAS,EAAI,EAC9E,CAAC,CACE,OAAQ,GAAS,EAAK,OAAS,CAAC,CAAC,CACjC,KAAK,IAAI,EAAE,GAGlB,GAAG,EAAO,OAAO,IAAK,GAAU,YAAY,GAAO,EACnD,GAAG,EAAO,SAAS,IAAK,GAAY,cAAc,GAAS,CAE/C,CAAC,CAAC,KAAK;CAAI,EAAE,GAC7B,CAEA,SAAS,EAAU,EAAe,EAAsB,CACtD,MAAO,GAAG,EAAM,GAAG,IAAO,IAAU,EAAI,GAAK,KAC/C,CAEA,SAAgB,EAAgB,EAAmB,EAA0B,CAC3E,OAAO,EAAM,OAAO,IACjB,GACC,GAAG,EAAO,IAAI,EAAM,KAAK,OAAS,EAAI,GAAG,EAAM,KAAK,KAAK,GAAG,EAAE,IAAM,KAAK,EAAM,UAAU,EAAsB,CAAK,GACxH,CACF,CAGA,SAAS,EAAsB,EAAiC,CAC9D,GAAI,EAAM,OAAS,gBAAiB,MAAO,GAC3C,IAAM,EAAiB,CAAC,GAAG,IAAI,IAAI,EAAM,OAAO,KAAK,CAAC,CAAC,IAAK,GAAgB,EAAY,OAAO,CAAC,CAAC,EACjG,OAAO,EAAe,OAAS,EAAI,KAAK,EAAe,KAAK,IAAI,EAAE,GAAK,EACzE,CAEA,SAAgB,EAAmB,EAAe,EAAkB,EAAwB,CAC1F,IAAM,kBAAU,IAAI,IACd,kBAAe,IAAI,IACzB,IAAK,IAAM,KAAM,GACd,EAAQ,IAAI,CAAE,EAAI,EAAe,EAAA,CAAS,IAAI,CAAE,EAE/C,EAAa,KAAO,GACtB,EAAO,KAAK,aAAa,EAAS,QAAQ,CAAC,GAAG,CAAY,CAAC,CAAC,KAAK,IAAI,GAAG,CAE5E"}