@exercode/problem-utils 3.0.0 → 3.1.1

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 (102) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/checkAllProblems.cjs +1 -1
  3. package/dist/cli/checkAllProblems.js +1 -1
  4. package/dist/cli/exercodeProblem.cjs +8 -2
  5. package/dist/cli/exercodeProblem.cjs.map +1 -1
  6. package/dist/cli/exercodeProblem.js +8 -2
  7. package/dist/cli/exercodeProblem.js.map +1 -1
  8. package/dist/cli/runSingleHarness.js.map +1 -1
  9. package/dist/cli/validateLearningMaterial.cjs +2 -0
  10. package/dist/cli/validateLearningMaterial.cjs.map +1 -0
  11. package/dist/cli/validateLearningMaterial.d.ts +7 -0
  12. package/dist/cli/validateLearningMaterial.js +2 -0
  13. package/dist/cli/validateLearningMaterial.js.map +1 -0
  14. package/dist/helpers/judgeByStaticAnalysis.cjs +3 -3
  15. package/dist/helpers/judgeByStaticAnalysis.js +3 -3
  16. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
  17. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  18. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
  19. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  20. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  21. package/dist/helpers/runHarnessProcess.d.ts +4 -1
  22. package/dist/helpers/runHarnessProcess.js.map +1 -1
  23. package/dist/helpers/spawnWithLimits.cjs +2 -0
  24. package/dist/helpers/spawnWithLimits.cjs.map +1 -0
  25. package/dist/helpers/spawnWithLimits.d.ts +27 -0
  26. package/dist/helpers/spawnWithLimits.js +2 -0
  27. package/dist/helpers/spawnWithLimits.js.map +1 -0
  28. package/dist/helpers/spawnWithTimeout.cjs +4 -0
  29. package/dist/helpers/spawnWithTimeout.cjs.map +1 -0
  30. package/dist/helpers/spawnWithTimeout.d.ts +19 -0
  31. package/dist/helpers/spawnWithTimeout.js +4 -0
  32. package/dist/helpers/spawnWithTimeout.js.map +1 -0
  33. package/dist/helpers/stdioJudgeRules.cjs +2 -0
  34. package/dist/helpers/stdioJudgeRules.cjs.map +1 -0
  35. package/dist/helpers/stdioJudgeRules.d.ts +4 -0
  36. package/dist/helpers/stdioJudgeRules.js +2 -0
  37. package/dist/helpers/stdioJudgeRules.js.map +1 -0
  38. package/dist/helpers/temporaryProblemDirCopy.cjs +1 -1
  39. package/dist/helpers/temporaryProblemDirCopy.js +1 -1
  40. package/dist/index.cjs +1 -1
  41. package/dist/index.d.ts +5 -0
  42. package/dist/index.js +1 -1
  43. package/dist/learningMaterial/frontmatter.cjs +2 -0
  44. package/dist/learningMaterial/frontmatter.cjs.map +1 -0
  45. package/dist/learningMaterial/frontmatter.d.ts +8 -0
  46. package/dist/learningMaterial/frontmatter.js +2 -0
  47. package/dist/learningMaterial/frontmatter.js.map +1 -0
  48. package/dist/learningMaterial/fsHelpers.cjs +2 -0
  49. package/dist/learningMaterial/fsHelpers.cjs.map +1 -0
  50. package/dist/learningMaterial/fsHelpers.d.ts +30 -0
  51. package/dist/learningMaterial/fsHelpers.js +2 -0
  52. package/dist/learningMaterial/fsHelpers.js.map +1 -0
  53. package/dist/learningMaterial/schemas.cjs +2 -0
  54. package/dist/learningMaterial/schemas.cjs.map +1 -0
  55. package/dist/learningMaterial/schemas.d.ts +241 -0
  56. package/dist/learningMaterial/schemas.js +2 -0
  57. package/dist/learningMaterial/schemas.js.map +1 -0
  58. package/dist/learningMaterial/validateContest.cjs +2 -0
  59. package/dist/learningMaterial/validateContest.cjs.map +1 -0
  60. package/dist/learningMaterial/validateContest.d.ts +16 -0
  61. package/dist/learningMaterial/validateContest.js +2 -0
  62. package/dist/learningMaterial/validateContest.js.map +1 -0
  63. package/dist/learningMaterial/validateCourse.cjs +2 -0
  64. package/dist/learningMaterial/validateCourse.cjs.map +1 -0
  65. package/dist/learningMaterial/validateCourse.d.ts +11 -0
  66. package/dist/learningMaterial/validateCourse.js +2 -0
  67. package/dist/learningMaterial/validateCourse.js.map +1 -0
  68. package/dist/learningMaterial/validateMaterial.cjs +4 -0
  69. package/dist/learningMaterial/validateMaterial.cjs.map +1 -0
  70. package/dist/learningMaterial/validateMaterial.d.ts +34 -0
  71. package/dist/learningMaterial/validateMaterial.js +4 -0
  72. package/dist/learningMaterial/validateMaterial.js.map +1 -0
  73. package/dist/learningMaterial/validateProblem.cjs +2 -0
  74. package/dist/learningMaterial/validateProblem.cjs.map +1 -0
  75. package/dist/learningMaterial/validateProblem.d.ts +6 -0
  76. package/dist/learningMaterial/validateProblem.js +2 -0
  77. package/dist/learningMaterial/validateProblem.js.map +1 -0
  78. package/dist/learningMaterial/validationResult.cjs +3 -0
  79. package/dist/learningMaterial/validationResult.cjs.map +1 -0
  80. package/dist/learningMaterial/validationResult.d.ts +9 -0
  81. package/dist/learningMaterial/validationResult.js +3 -0
  82. package/dist/learningMaterial/validationResult.js.map +1 -0
  83. package/dist/presets/command.cjs +1 -1
  84. package/dist/presets/command.cjs.map +1 -1
  85. package/dist/presets/command.js +1 -1
  86. package/dist/presets/command.js.map +1 -1
  87. package/dist/presets/evaluation.cjs +2 -2
  88. package/dist/presets/evaluation.js +2 -2
  89. package/dist/presets/guiCommand.cjs +2 -2
  90. package/dist/presets/guiCommand.cjs.map +1 -1
  91. package/dist/presets/guiCommand.js +2 -2
  92. package/dist/presets/guiCommand.js.map +1 -1
  93. package/dist/presets/stdio.cjs +1 -1
  94. package/dist/presets/stdio.cjs.map +1 -1
  95. package/dist/presets/stdio.js +1 -1
  96. package/dist/presets/stdio.js.map +1 -1
  97. package/package.json +2 -1
  98. package/dist/helpers/spawnSyncWithTimeout.cjs +0 -2
  99. package/dist/helpers/spawnSyncWithTimeout.cjs.map +0 -1
  100. package/dist/helpers/spawnSyncWithTimeout.d.ts +0 -5
  101. package/dist/helpers/spawnSyncWithTimeout.js +0 -2
  102. package/dist/helpers/spawnSyncWithTimeout.js.map +0 -1
@@ -0,0 +1,30 @@
1
+ export interface SourceFile {
2
+ path: string;
3
+ data: string;
4
+ }
5
+ export declare function isFile(path: string): Promise<boolean>;
6
+ /** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */
7
+ export declare function isRegularFile(path: string): Promise<boolean>;
8
+ /** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */
9
+ export declare function isRegularDirectory(path: string): Promise<boolean>;
10
+ export declare function isDirectory(path: string): Promise<boolean>;
11
+ /**
12
+ * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`
13
+ * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic
14
+ * links or a nested course (whose problems belong to that nearer course). The result maps each
15
+ * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID
16
+ * defined more than once.
17
+ */
18
+ export declare function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>>;
19
+ /**
20
+ * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern
21
+ * aborts discovery of the whole repository, and an ID defined more than once is a conflict.
22
+ */
23
+ export declare function reportProblemDefinitionIssues(definitionPathsById: ReadonlyMap<string, string[]>, errors: string[]): void;
24
+ /** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */
25
+ export declare function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean>;
26
+ /** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */
27
+ export declare function collectProblemIdsFromDirectory(problemsDirectoryPath: string, errors: string[]): Promise<Set<string> | undefined>;
28
+ export declare function reportDanglingProblemReferences(problemIds: Iterable<string>, availableProblemIds: ReadonlySet<string>, problemsDirectoryPath: string | undefined, errors: string[]): void;
29
+ /** Reads all text files under a directory, with paths relative to the given root. */
30
+ export declare function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]>;
@@ -0,0 +1,2 @@
1
+ import{LEARNING_MATERIAL_ID_REGEX as e}from"./schemas.js";import{basename as t,extname as n,join as r,relative as i}from"node:path";import{lstat as a,readFile as o,readdir as s,stat as c}from"node:fs/promises";const l=new Set([`__pycache__`]),u=new Set([``,`.class`,`.exe`,`.jar`,`.out`,`.pyc`,`.png`,`.gif`]);async function d(e){try{return(await c(e)).isFile()}catch{return!1}}async function f(e){try{return(await a(e)).isFile()}catch{return!1}}async function p(e){try{return(await a(e)).isDirectory()}catch{return!1}}async function m(e){try{return(await c(e)).isDirectory()}catch{return!1}}async function h(e){let t=new Map;return await g(e,e,t),t}async function g(e,n,a){let o=await s(n,{withFileTypes:!0});if(!(n!==e&&o.some(e=>e.isFile()&&(e.name===`course.yaml`||e.name===`course.yml`))))for(let s of o)if(s.isFile()){let o=s.name===`problem.md`?t(n):s.name.endsWith(`.problem.md`)?s.name.slice(0,-11).toLowerCase():void 0;if(o===void 0)continue;let c=a.get(o)??[];c.push(i(e,r(n,s.name))),a.set(o,c)}else s.isDirectory()&&!_(n,s.name)&&await g(e,r(n,s.name),a)}function _(e,n){return n.startsWith(`.`)||n===`node_modules`||n===`target`||n===`temp`&&t(e)===`judge-environments`}function v(t,n){for(let[r,i]of t)e.test(r)||n.push(`problem ID "${r}" (${i.join(`, `)}) must match ${e}; the judge aborts discovery on it`),i.length>1&&n.push(`problem ID "${r}" is defined more than once (${i.join(`, `)}); the judge rejects the conflict`)}async function y(e,t){return await p(e)?!0:(t.push(await m(e)?`problems directory is a symbolic link, which the judge does not traverse: ${e}`:`problems directory not found: ${e}`),!1)}async function b(e,t){if(!await y(e,t))return;let n=await h(e);return v(n,t),new Set(n.keys())}function x(e,t,n,r){let i=n===void 0?``:` under ${n}`;for(let n of new Set(e))t.has(n)||r.push(`problem "${n}" is referenced but does not exist${i}`)}async function S(e){let t=[];return await C(e,e,t),t.toSorted((e,t)=>e.path.localeCompare(t.path))}async function C(e,t,a){for(let c of await s(t,{withFileTypes:!0})){let s=r(t,c.name);c.isDirectory()?l.has(c.name)||await C(e,s,a):c.isFile()&&!u.has(n(c.name).toLowerCase())&&a.push({path:i(e,s),data:await o(s,`utf8`)})}}export{h as collectProblemDefinitions,b as collectProblemIdsFromDirectory,m as isDirectory,d as isFile,p as isRegularDirectory,f as isRegularFile,y as isUsableProblemsDirectory,S as readSourceFilesRecursively,x as reportDanglingProblemReferences,v as reportProblemDefinitionIssues};
2
+ //# sourceMappingURL=fsHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fsHelpers.js","names":[],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"kNASA,MAAM,EAA+C,IAAI,IAAI,CAAC,aAAa,CAAC,EACtE,EAA2C,IAAI,IAAI,CACvD,GACA,SACA,OACA,OACA,OACA,OACA,OACA,MACF,CAAC,EAOD,eAAsB,EAAO,EAAgC,CAC3D,GAAI,CAEF,OAAO,MADa,EAAK,CAAI,EAAA,CAChB,OAAO,CACtB,MAAQ,CACN,MAAO,EACT,CACF,CAGA,eAAsB,EAAc,EAAgC,CAClE,GAAI,CAEF,OAAO,MADa,EAAM,CAAI,EAAA,CACjB,OAAO,CACtB,MAAQ,CACN,MAAO,EACT,CACF,CAGA,eAAsB,EAAmB,EAAgC,CACvE,GAAI,CAEF,OAAO,MADa,EAAM,CAAI,EAAA,CACjB,YAAY,CAC3B,MAAQ,CACN,MAAO,EACT,CACF,CAEA,eAAsB,EAAY,EAAgC,CAChE,GAAI,CAEF,OAAO,MADa,EAAK,CAAI,EAAA,CAChB,YAAY,CAC3B,MAAQ,CACN,MAAO,EACT,CACF,CASA,eAAsB,EAA0B,EAA2D,CACzG,IAAM,EAAsB,IAAI,IAEhC,OADA,MAAM,EAA8B,EAAmB,EAAmB,CAAmB,EACtF,CACT,CAEA,eAAe,EACb,EACA,EACA,EACe,CACf,IAAM,EAAU,MAAM,EAAQ,EAAe,CAAE,cAAe,EAAK,CAAC,EAElE,SAAkB,GAClB,EAAQ,KAAM,GAAW,EAAO,OAAO,IAAM,EAAO,OAAS,eAAiB,EAAO,OAAS,aAAa,GAI7G,IAAK,IAAM,KAAU,EACnB,GAAI,EAAO,OAAO,EAAG,CACnB,IAAM,EACJ,EAAO,OAAS,aACZ,EAAS,CAAa,EACtB,EAAO,KAAK,SAAS,aAAa,EAEhC,EAAO,KAAK,MAAM,EAAG,GAAqB,CAAC,CAAC,YAAY,EACxD,IAAA,GACR,GAAI,IAAc,IAAA,GAAW,SAC7B,IAAM,EAAkB,EAAoB,IAAI,CAAS,GAAK,CAAC,EAC/D,EAAgB,KAAK,EAAS,EAAmB,EAAK,EAAe,EAAO,IAAI,CAAC,CAAC,EAClF,EAAoB,IAAI,EAAW,CAAe,CACpD,MAAW,EAAO,YAAY,GAAK,CAAC,EAAwB,EAAe,EAAO,IAAI,GACpF,MAAM,EAA8B,EAAmB,EAAK,EAAe,EAAO,IAAI,EAAG,CAAmB,CAGlH,CAGA,SAAS,EAAwB,EAA6B,EAAgC,CAC5F,OACE,EAAc,WAAW,GAAG,GAC5B,IAAkB,gBAClB,IAAkB,UACjB,IAAkB,QAAU,EAAS,CAAmB,IAAM,oBAEnE,CAMA,SAAgB,EACd,EACA,EACM,CACN,IAAK,GAAM,CAAC,EAAW,KAAoB,EACpC,EAA2B,KAAK,CAAS,GAC5C,EAAO,KACL,eAAe,EAAU,KAAK,EAAgB,KAAK,IAAI,EAAE,eAAe,EAA2B,mCACrG,EAEE,EAAgB,OAAS,GAC3B,EAAO,KACL,eAAe,EAAU,+BAA+B,EAAgB,KAAK,IAAI,EAAE,kCACrF,CAGN,CAGA,eAAsB,EAA0B,EAA+B,EAAoC,CAOjH,OANI,MAAM,EAAmB,CAAqB,EAAU,IAC5D,EAAO,KACJ,MAAM,EAAY,CAAqB,EACpC,6EAA6E,IAC7E,iCAAiC,GACvC,EACO,GACT,CAGA,eAAsB,EACpB,EACA,EACkC,CAClC,GAAI,CAAE,MAAM,EAA0B,EAAuB,CAAM,EAAI,OACvE,IAAM,EAAsB,MAAM,EAA0B,CAAqB,EAEjF,OADA,EAA8B,EAAqB,CAAM,EAClD,IAAI,IAAI,EAAoB,KAAK,CAAC,CAC3C,CAEA,SAAgB,EACd,EACA,EACA,EACA,EACM,CACN,IAAM,EAAW,IAA0B,IAAA,GAAY,GAAK,UAAU,IACtE,IAAK,IAAM,KAAa,IAAI,IAAI,CAAU,EACnC,EAAoB,IAAI,CAAS,GACpC,EAAO,KAAK,YAAY,EAAU,oCAAoC,GAAU,CAGtF,CAGA,eAAsB,EAA2B,EAAkD,CACjG,IAAM,EAAsB,CAAC,EAE7B,OADA,MAAM,EAAmB,EAAmB,EAAmB,CAAK,EAC7D,EAAM,UAAU,EAAI,IAAO,EAAG,KAAK,cAAc,EAAG,IAAI,CAAC,CAClE,CAEA,eAAe,EACb,EACA,EACA,EACe,CACf,IAAK,IAAM,KAAU,MAAM,EAAQ,EAAe,CAAE,cAAe,EAAK,CAAC,EAAG,CAC1E,IAAM,EAAY,EAAK,EAAe,EAAO,IAAI,EAC7C,EAAO,YAAY,EAChB,EAAwB,IAAI,EAAO,IAAI,GAC1C,MAAM,EAAmB,EAAmB,EAAW,CAAK,EAErD,EAAO,OAAO,GAAK,CAAC,EAAoB,IAAI,EAAQ,EAAO,IAAI,CAAC,CAAC,YAAY,CAAC,GACvF,EAAM,KAAK,CAAE,KAAM,EAAS,EAAmB,CAAS,EAAG,KAAM,MAAM,EAAS,EAAW,MAAM,CAAE,CAAC,CAExG,CACF"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("../types/language.cjs"),t=require("../types/problem.cjs");let n=require("zod");const r=/^[0-9_a-z-]+$/,i=n.z.string().regex(r),a=Object.keys(e.languageIdToDefinition),o=n.z.enum(a),s=n.z.string().regex(/^\w+$/),c=n.z.strictObject({name:n.z.string().min(1),type:n.z.enum([`prompt_study`]).optional(),timeLimitMs:n.z.number().int().min(0).optional(),memoryLimitByte:n.z.number().int().min(0).optional(),requiredRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenTextsInCode:t.codeRuleSchema.array().default([]),canCreateFiles:n.z.boolean().optional(),isEditorDisabled:n.z.boolean().optional(),isAttachedFileRequired:n.z.boolean().optional(),isManualScoringRequired:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),requiredEnvironmentVariables:n.z.array(s).default([]),requiredOutputFilePaths:n.z.string().min(1).array().default([]),requiredSubmissionFilePaths:n.z.string().min(1).array().default([])}),l={availableLanguageIds:o.array().optional(),areTestCasesHidden:n.z.boolean().optional(),isProblemGradingResultHidden:n.z.boolean().optional(),isAutoFormatDisabled:n.z.boolean().optional(),isCopyAndPasteDisabled:n.z.boolean().optional(),isDebugHintDisabled:n.z.boolean().optional(),isFixHintDisabled:n.z.boolean().optional(),isDiffHintDisabled:n.z.boolean().optional(),debugHintWaitingSeconds:n.z.number().min(0).optional(),fixHintWaitingSeconds:n.z.number().min(0).optional(),diffHintWaitingSeconds:n.z.number().min(0).optional(),submissionOpenedAt:n.z.iso.datetime({offset:!0}).optional(),submissionSoftClosedAt:n.z.iso.datetime({offset:!0}).optional(),submissionHardClosedAt:n.z.iso.datetime({offset:!0}).optional(),isAutoTranslationDisabled:n.z.boolean().optional(),isModelAnswerShownAfterDeadline:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),isMaterialChatDisabled:n.z.boolean().optional()},u=n.z.strictObject({...l,name:n.z.string().min(1),description:n.z.string(),author:n.z.string().min(1).optional(),isMotivationFeatureEnabled:n.z.boolean().optional(),isPublic:n.z.boolean().optional(),lectures:n.z.array(n.z.object({id:i,name:n.z.string().min(1),description:n.z.string()})).optional()}),d=n.z.string().array().min(1).refine(e=>e.length===new Set(e).size,{error:`options must contain unique values`}),f=n.z.string().regex(/^\d+$/,{error:`must be a non-negative integer index (quoted numbers such as "0" are accepted)`}).transform(Number),p={id:i,isResubmittable:n.z.boolean().optional(),isSurvey:n.z.boolean().optional(),explanation:n.z.string().optional(),hint:n.z.string().optional()},m={...p,type:n.z.literal(`select`),options:d,answerIndex:n.z.union([n.z.number().int().nonnegative(),f,n.z.number().int().nonnegative().array(),f.array()]).optional()},h={...p,type:n.z.literal(`select_multiple`),options:d,answerIndices:n.z.union([n.z.number().int().nonnegative().array(),f.array()]).optional()},g={...p,type:n.z.literal(`text`),answerPattern:n.z.string().min(1).optional(),modelAnswer:n.z.string().optional()},_=n.z.discriminatedUnion(`type`,[n.z.strictObject({...m,question:n.z.string()}),n.z.strictObject({...h,question:n.z.string()}),n.z.strictObject({...g,question:n.z.string()})]),v=n.z.discriminatedUnion(`type`,[n.z.strictObject({...m,question:n.z.string().optional()}),n.z.strictObject({...h,question:n.z.string().optional()}),n.z.strictObject({...g,question:n.z.string().optional()})]),y=n.z.strictObject({...l,name:n.z.string().min(1),problems:n.z.object({id:i,courseId:i.optional()}).array().optional(),questions:n.z.array(v).optional(),turtleGraphicsQuestions:n.z.object({id:i}).array().optional(),isExamination:n.z.boolean().optional(),isMockExamination:n.z.boolean().optional(),isRealtimeSurvey:n.z.boolean().optional()}),b=n.z.strictObject({id:i,name:n.z.string().min(1),openedAt:n.z.iso.datetime({offset:!0}),closedAt:n.z.iso.datetime({offset:!0}),password:n.z.string().min(1).optional()}).refine(e=>new Date(e.openedAt)<new Date(e.closedAt),{error:`openedAt must be earlier than closedAt`}),x=n.z.strictObject({id:i,score:n.z.number().int().nonnegative(),courseId:i.optional(),areTestCasesHidden:l.areTestCasesHidden,isDebugHintDisabled:l.isDebugHintDisabled,isFixHintDisabled:l.isFixHintDisabled,isDiffHintDisabled:l.isDiffHintDisabled,debugHintWaitingSeconds:l.debugHintWaitingSeconds,fixHintWaitingSeconds:l.fixHintWaitingSeconds,diffHintWaitingSeconds:l.diffHintWaitingSeconds,isMaterialChatDisabled:l.isMaterialChatDisabled}),S=n.z.strictObject({...l,name:n.z.string().min(1),description:n.z.string().optional(),adminEmails:n.z.array(n.z.string().trim().email()).optional(),showsProblemsAfterClose:n.z.boolean().optional(),divisions:n.z.array(b).min(1),problems:n.z.array(x).min(1)});exports.CONTEST_MATERIAL_FILE_SUFFIX=`.contest.yaml`,exports.LEARNING_MATERIAL_ID_REGEX=r,exports.availableLanguageIds=a,exports.contestFileSchema=S,exports.courseFileSchema=u,exports.learningMaterialIdSchema=i,exports.materialFrontmatterSchema=y,exports.problemFrontmatterSchema=c,exports.questionInCodeBlockSchema=_;
2
+ //# sourceMappingURL=schemas.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.cjs","names":["z","languageIdToDefinition","codeRuleSchema"],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { languageIdToDefinition } from '../types/language.js';\nimport { codeRuleSchema } from '../types/problem.js';\n\n// Schemas in this file mirror the Exercode judge importer so that generated materials validate\n// exactly like the importer would parse them.\n\nexport const LEARNING_MATERIAL_ID_REGEX = /^[0-9_a-z-]+$/;\n\nexport const learningMaterialIdSchema = z.string().regex(LEARNING_MATERIAL_ID_REGEX);\n\nexport const availableLanguageIds = Object.keys(languageIdToDefinition);\nconst availableLanguageIdSchema = z.enum(availableLanguageIds);\n\nconst environmentVariableNameSchema = z.string().regex(/^\\w+$/);\n\n// Mirrors the judge server's strict problem frontmatter schema; keys the server removed with the\n// v1 judge (judgeEnvironmentId, generalJudgeEnvironmentConfigOverrides, testCases, isGui) are\n// rejected here as unknown keys as well.\nexport const problemFrontmatterSchema = z.strictObject({\n name: z.string().min(1),\n type: z.enum(['prompt_study']).optional(),\n timeLimitMs: z.number().int().min(0).optional(),\n memoryLimitByte: z.number().int().min(0).optional(),\n requiredRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenTextsInCode: codeRuleSchema.array().default([]),\n canCreateFiles: z.boolean().optional(),\n isEditorDisabled: z.boolean().optional(),\n isAttachedFileRequired: z.boolean().optional(),\n isManualScoringRequired: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n requiredEnvironmentVariables: z.array(environmentVariableNameSchema).default([]),\n requiredOutputFilePaths: z.string().min(1).array().default([]),\n requiredSubmissionFilePaths: z.string().min(1).array().default([]),\n});\n\nexport type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;\n\nconst materialConfigShape = {\n availableLanguageIds: availableLanguageIdSchema.array().optional(),\n areTestCasesHidden: z.boolean().optional(),\n isProblemGradingResultHidden: z.boolean().optional(),\n isAutoFormatDisabled: z.boolean().optional(),\n isCopyAndPasteDisabled: z.boolean().optional(),\n isDebugHintDisabled: z.boolean().optional(),\n isFixHintDisabled: z.boolean().optional(),\n isDiffHintDisabled: z.boolean().optional(),\n debugHintWaitingSeconds: z.number().min(0).optional(),\n fixHintWaitingSeconds: z.number().min(0).optional(),\n diffHintWaitingSeconds: z.number().min(0).optional(),\n // z.iso.datetime is the zod v4 API (this repo pins zod 4); z.string().datetime is the v3 form.\n submissionOpenedAt: z.iso.datetime({ offset: true }).optional(),\n submissionSoftClosedAt: z.iso.datetime({ offset: true }).optional(),\n submissionHardClosedAt: z.iso.datetime({ offset: true }).optional(),\n isAutoTranslationDisabled: z.boolean().optional(),\n isModelAnswerShownAfterDeadline: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n isMaterialChatDisabled: z.boolean().optional(),\n} as const;\n\nexport const courseFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string(),\n author: z.string().min(1).optional(),\n isMotivationFeatureEnabled: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n // The importer strips unknown keys of a lecture entry (plain object) while the course object is strict.\n lectures: z\n .array(\n z.object({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n description: z.string(),\n })\n )\n .optional(),\n});\n\nexport type CourseFile = z.infer<typeof courseFileSchema>;\n\nconst uniqueOptionsSchema = z\n .string()\n .array()\n .min(1)\n // zod 4 names the custom-message option `error` (`message` is the deprecated zod 3 spelling).\n .refine((options) => options.length === new Set(options).size, { error: 'options must contain unique values' });\n\n// A quoted index is coerced like the judge does, but a non-numeric string is a schema error rather\n// than a NaN that would surface as an out-of-bounds index.\nconst answerIndexStringSchema = z\n .string()\n .regex(/^\\d+$/, { error: 'must be a non-negative integer index (quoted numbers such as \"0\" are accepted)' })\n .transform(Number);\n\nconst questionBaseShape = {\n id: learningMaterialIdSchema,\n isResubmittable: z.boolean().optional(),\n isSurvey: z.boolean().optional(),\n explanation: z.string().optional(),\n hint: z.string().optional(),\n} as const;\n\nconst selectQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select'),\n options: uniqueOptionsSchema,\n answerIndex: z\n .union([\n z.number().int().nonnegative(),\n answerIndexStringSchema,\n z.number().int().nonnegative().array(),\n answerIndexStringSchema.array(),\n ])\n .optional(),\n} as const;\n\nconst selectMultipleQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select_multiple'),\n options: uniqueOptionsSchema,\n answerIndices: z.union([z.number().int().nonnegative().array(), answerIndexStringSchema.array()]).optional(),\n} as const;\n\nconst textQuestionShape = {\n ...questionBaseShape,\n type: z.literal('text'),\n answerPattern: z.string().min(1).optional(),\n modelAnswer: z.string().optional(),\n} as const;\n\nexport const questionInCodeBlockSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string() }),\n z.strictObject({ ...textQuestionShape, question: z.string() }),\n]);\n\n// The judge still supports question definitions in material frontmatter where `question` is optional.\nconst questionInFrontmatterSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...textQuestionShape, question: z.string().optional() }),\n]);\n\nexport type MaterialQuestion = z.infer<typeof questionInFrontmatterSchema>;\n\nexport const materialFrontmatterSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n problems: z\n .object({ id: learningMaterialIdSchema, courseId: learningMaterialIdSchema.optional() })\n .array()\n .optional(),\n questions: z.array(questionInFrontmatterSchema).optional(),\n turtleGraphicsQuestions: z.object({ id: learningMaterialIdSchema }).array().optional(),\n isExamination: z.boolean().optional(),\n isMockExamination: z.boolean().optional(),\n isRealtimeSurvey: z.boolean().optional(),\n});\n\nexport const CONTEST_MATERIAL_FILE_SUFFIX = '.contest.yaml';\n\nconst contestDivisionSchema = z\n .strictObject({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n openedAt: z.iso.datetime({ offset: true }),\n closedAt: z.iso.datetime({ offset: true }),\n password: z.string().min(1).optional(),\n })\n .refine((division) => new Date(division.openedAt) < new Date(division.closedAt), {\n error: 'openedAt must be earlier than closedAt',\n });\n\nconst contestProblemSchema = z.strictObject({\n id: learningMaterialIdSchema,\n score: z.number().int().nonnegative(),\n courseId: learningMaterialIdSchema.optional(),\n // Per-problem overrides reuse the shared material-config fields, mirroring the judge schema.\n areTestCasesHidden: materialConfigShape.areTestCasesHidden,\n isDebugHintDisabled: materialConfigShape.isDebugHintDisabled,\n isFixHintDisabled: materialConfigShape.isFixHintDisabled,\n isDiffHintDisabled: materialConfigShape.isDiffHintDisabled,\n debugHintWaitingSeconds: materialConfigShape.debugHintWaitingSeconds,\n fixHintWaitingSeconds: materialConfigShape.fixHintWaitingSeconds,\n diffHintWaitingSeconds: materialConfigShape.diffHintWaitingSeconds,\n isMaterialChatDisabled: materialConfigShape.isMaterialChatDisabled,\n});\n\nexport const contestFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string().optional(),\n // Exercode reads the contest administrators itself and strips the key before the judge's strict schema.\n adminEmails: z.array(z.string().trim().email()).optional(),\n showsProblemsAfterClose: z.boolean().optional(),\n divisions: z.array(contestDivisionSchema).min(1),\n problems: z.array(contestProblemSchema).min(1),\n});\n"],"mappings":"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"}
@@ -0,0 +1,241 @@
1
+ import { z } from 'zod';
2
+ export declare const LEARNING_MATERIAL_ID_REGEX: RegExp;
3
+ export declare const learningMaterialIdSchema: z.ZodString;
4
+ export declare const availableLanguageIds: string[];
5
+ export declare const problemFrontmatterSchema: z.ZodObject<{
6
+ name: z.ZodString;
7
+ type: z.ZodOptional<z.ZodEnum<{
8
+ prompt_study: "prompt_study";
9
+ }>>;
10
+ timeLimitMs: z.ZodOptional<z.ZodNumber>;
11
+ memoryLimitByte: z.ZodOptional<z.ZodNumber>;
12
+ requiredRegExpsInCode: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
13
+ pattern: z.ZodString;
14
+ message: z.ZodString;
15
+ }, z.core.$strip>]>>>;
16
+ forbiddenRegExpsInCode: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
17
+ pattern: z.ZodString;
18
+ message: z.ZodString;
19
+ }, z.core.$strip>]>>>;
20
+ forbiddenTextsInCode: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
21
+ pattern: z.ZodString;
22
+ message: z.ZodString;
23
+ }, z.core.$strip>]>>>;
24
+ canCreateFiles: z.ZodOptional<z.ZodBoolean>;
25
+ isEditorDisabled: z.ZodOptional<z.ZodBoolean>;
26
+ isAttachedFileRequired: z.ZodOptional<z.ZodBoolean>;
27
+ isManualScoringRequired: z.ZodOptional<z.ZodBoolean>;
28
+ isVotable: z.ZodOptional<z.ZodBoolean>;
29
+ requiredEnvironmentVariables: z.ZodDefault<z.ZodArray<z.ZodString>>;
30
+ requiredOutputFilePaths: z.ZodDefault<z.ZodArray<z.ZodString>>;
31
+ requiredSubmissionFilePaths: z.ZodDefault<z.ZodArray<z.ZodString>>;
32
+ }, z.core.$strict>;
33
+ export type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;
34
+ export declare const courseFileSchema: z.ZodObject<{
35
+ availableLanguageIds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
36
+ [x: string]: string;
37
+ }>>>;
38
+ areTestCasesHidden: z.ZodOptional<z.ZodBoolean>;
39
+ isProblemGradingResultHidden: z.ZodOptional<z.ZodBoolean>;
40
+ isAutoFormatDisabled: z.ZodOptional<z.ZodBoolean>;
41
+ isCopyAndPasteDisabled: z.ZodOptional<z.ZodBoolean>;
42
+ isDebugHintDisabled: z.ZodOptional<z.ZodBoolean>;
43
+ isFixHintDisabled: z.ZodOptional<z.ZodBoolean>;
44
+ isDiffHintDisabled: z.ZodOptional<z.ZodBoolean>;
45
+ debugHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
46
+ fixHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
47
+ diffHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
48
+ submissionOpenedAt: z.ZodOptional<z.ZodISODateTime>;
49
+ submissionSoftClosedAt: z.ZodOptional<z.ZodISODateTime>;
50
+ submissionHardClosedAt: z.ZodOptional<z.ZodISODateTime>;
51
+ isAutoTranslationDisabled: z.ZodOptional<z.ZodBoolean>;
52
+ isModelAnswerShownAfterDeadline: z.ZodOptional<z.ZodBoolean>;
53
+ isVotable: z.ZodOptional<z.ZodBoolean>;
54
+ isMaterialChatDisabled: z.ZodOptional<z.ZodBoolean>;
55
+ name: z.ZodString;
56
+ description: z.ZodString;
57
+ author: z.ZodOptional<z.ZodString>;
58
+ isMotivationFeatureEnabled: z.ZodOptional<z.ZodBoolean>;
59
+ isPublic: z.ZodOptional<z.ZodBoolean>;
60
+ lectures: z.ZodOptional<z.ZodArray<z.ZodObject<{
61
+ id: z.ZodString;
62
+ name: z.ZodString;
63
+ description: z.ZodString;
64
+ }, z.core.$strip>>>;
65
+ }, z.core.$strict>;
66
+ export type CourseFile = z.infer<typeof courseFileSchema>;
67
+ export declare const questionInCodeBlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
68
+ id: z.ZodString;
69
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
70
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
71
+ explanation: z.ZodOptional<z.ZodString>;
72
+ hint: z.ZodOptional<z.ZodString>;
73
+ type: z.ZodLiteral<"select">;
74
+ options: z.ZodArray<z.ZodString>;
75
+ answerIndex: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>>]>>;
76
+ question: z.ZodString;
77
+ }, z.core.$strict>, z.ZodObject<{
78
+ id: z.ZodString;
79
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
80
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
81
+ explanation: z.ZodOptional<z.ZodString>;
82
+ hint: z.ZodOptional<z.ZodString>;
83
+ type: z.ZodLiteral<"select_multiple">;
84
+ options: z.ZodArray<z.ZodString>;
85
+ answerIndices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>>]>>;
86
+ question: z.ZodString;
87
+ }, z.core.$strict>, z.ZodObject<{
88
+ id: z.ZodString;
89
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
90
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
91
+ explanation: z.ZodOptional<z.ZodString>;
92
+ hint: z.ZodOptional<z.ZodString>;
93
+ type: z.ZodLiteral<"text">;
94
+ answerPattern: z.ZodOptional<z.ZodString>;
95
+ modelAnswer: z.ZodOptional<z.ZodString>;
96
+ question: z.ZodString;
97
+ }, z.core.$strict>], "type">;
98
+ declare const questionInFrontmatterSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
99
+ id: z.ZodString;
100
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
101
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
102
+ explanation: z.ZodOptional<z.ZodString>;
103
+ hint: z.ZodOptional<z.ZodString>;
104
+ type: z.ZodLiteral<"select">;
105
+ options: z.ZodArray<z.ZodString>;
106
+ answerIndex: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>>]>>;
107
+ question: z.ZodOptional<z.ZodString>;
108
+ }, z.core.$strict>, z.ZodObject<{
109
+ id: z.ZodString;
110
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
111
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
112
+ explanation: z.ZodOptional<z.ZodString>;
113
+ hint: z.ZodOptional<z.ZodString>;
114
+ type: z.ZodLiteral<"select_multiple">;
115
+ options: z.ZodArray<z.ZodString>;
116
+ answerIndices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>>]>>;
117
+ question: z.ZodOptional<z.ZodString>;
118
+ }, z.core.$strict>, z.ZodObject<{
119
+ id: z.ZodString;
120
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
121
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
122
+ explanation: z.ZodOptional<z.ZodString>;
123
+ hint: z.ZodOptional<z.ZodString>;
124
+ type: z.ZodLiteral<"text">;
125
+ answerPattern: z.ZodOptional<z.ZodString>;
126
+ modelAnswer: z.ZodOptional<z.ZodString>;
127
+ question: z.ZodOptional<z.ZodString>;
128
+ }, z.core.$strict>], "type">;
129
+ export type MaterialQuestion = z.infer<typeof questionInFrontmatterSchema>;
130
+ export declare const materialFrontmatterSchema: z.ZodObject<{
131
+ availableLanguageIds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
132
+ [x: string]: string;
133
+ }>>>;
134
+ areTestCasesHidden: z.ZodOptional<z.ZodBoolean>;
135
+ isProblemGradingResultHidden: z.ZodOptional<z.ZodBoolean>;
136
+ isAutoFormatDisabled: z.ZodOptional<z.ZodBoolean>;
137
+ isCopyAndPasteDisabled: z.ZodOptional<z.ZodBoolean>;
138
+ isDebugHintDisabled: z.ZodOptional<z.ZodBoolean>;
139
+ isFixHintDisabled: z.ZodOptional<z.ZodBoolean>;
140
+ isDiffHintDisabled: z.ZodOptional<z.ZodBoolean>;
141
+ debugHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
142
+ fixHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
143
+ diffHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
144
+ submissionOpenedAt: z.ZodOptional<z.ZodISODateTime>;
145
+ submissionSoftClosedAt: z.ZodOptional<z.ZodISODateTime>;
146
+ submissionHardClosedAt: z.ZodOptional<z.ZodISODateTime>;
147
+ isAutoTranslationDisabled: z.ZodOptional<z.ZodBoolean>;
148
+ isModelAnswerShownAfterDeadline: z.ZodOptional<z.ZodBoolean>;
149
+ isVotable: z.ZodOptional<z.ZodBoolean>;
150
+ isMaterialChatDisabled: z.ZodOptional<z.ZodBoolean>;
151
+ name: z.ZodString;
152
+ problems: z.ZodOptional<z.ZodArray<z.ZodObject<{
153
+ id: z.ZodString;
154
+ courseId: z.ZodOptional<z.ZodString>;
155
+ }, z.core.$strip>>>;
156
+ questions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
157
+ id: z.ZodString;
158
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
159
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
160
+ explanation: z.ZodOptional<z.ZodString>;
161
+ hint: z.ZodOptional<z.ZodString>;
162
+ type: z.ZodLiteral<"select">;
163
+ options: z.ZodArray<z.ZodString>;
164
+ answerIndex: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>, z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>>]>>;
165
+ question: z.ZodOptional<z.ZodString>;
166
+ }, z.core.$strict>, z.ZodObject<{
167
+ id: z.ZodString;
168
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
169
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
170
+ explanation: z.ZodOptional<z.ZodString>;
171
+ hint: z.ZodOptional<z.ZodString>;
172
+ type: z.ZodLiteral<"select_multiple">;
173
+ options: z.ZodArray<z.ZodString>;
174
+ answerIndices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>>]>>;
175
+ question: z.ZodOptional<z.ZodString>;
176
+ }, z.core.$strict>, z.ZodObject<{
177
+ id: z.ZodString;
178
+ isResubmittable: z.ZodOptional<z.ZodBoolean>;
179
+ isSurvey: z.ZodOptional<z.ZodBoolean>;
180
+ explanation: z.ZodOptional<z.ZodString>;
181
+ hint: z.ZodOptional<z.ZodString>;
182
+ type: z.ZodLiteral<"text">;
183
+ answerPattern: z.ZodOptional<z.ZodString>;
184
+ modelAnswer: z.ZodOptional<z.ZodString>;
185
+ question: z.ZodOptional<z.ZodString>;
186
+ }, z.core.$strict>], "type">>>;
187
+ turtleGraphicsQuestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
188
+ id: z.ZodString;
189
+ }, z.core.$strip>>>;
190
+ isExamination: z.ZodOptional<z.ZodBoolean>;
191
+ isMockExamination: z.ZodOptional<z.ZodBoolean>;
192
+ isRealtimeSurvey: z.ZodOptional<z.ZodBoolean>;
193
+ }, z.core.$strict>;
194
+ export declare const CONTEST_MATERIAL_FILE_SUFFIX = ".contest.yaml";
195
+ export declare const contestFileSchema: z.ZodObject<{
196
+ availableLanguageIds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
197
+ [x: string]: string;
198
+ }>>>;
199
+ areTestCasesHidden: z.ZodOptional<z.ZodBoolean>;
200
+ isProblemGradingResultHidden: z.ZodOptional<z.ZodBoolean>;
201
+ isAutoFormatDisabled: z.ZodOptional<z.ZodBoolean>;
202
+ isCopyAndPasteDisabled: z.ZodOptional<z.ZodBoolean>;
203
+ isDebugHintDisabled: z.ZodOptional<z.ZodBoolean>;
204
+ isFixHintDisabled: z.ZodOptional<z.ZodBoolean>;
205
+ isDiffHintDisabled: z.ZodOptional<z.ZodBoolean>;
206
+ debugHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
207
+ fixHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
208
+ diffHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
209
+ submissionOpenedAt: z.ZodOptional<z.ZodISODateTime>;
210
+ submissionSoftClosedAt: z.ZodOptional<z.ZodISODateTime>;
211
+ submissionHardClosedAt: z.ZodOptional<z.ZodISODateTime>;
212
+ isAutoTranslationDisabled: z.ZodOptional<z.ZodBoolean>;
213
+ isModelAnswerShownAfterDeadline: z.ZodOptional<z.ZodBoolean>;
214
+ isVotable: z.ZodOptional<z.ZodBoolean>;
215
+ isMaterialChatDisabled: z.ZodOptional<z.ZodBoolean>;
216
+ name: z.ZodString;
217
+ description: z.ZodOptional<z.ZodString>;
218
+ adminEmails: z.ZodOptional<z.ZodArray<z.ZodString>>;
219
+ showsProblemsAfterClose: z.ZodOptional<z.ZodBoolean>;
220
+ divisions: z.ZodArray<z.ZodObject<{
221
+ id: z.ZodString;
222
+ name: z.ZodString;
223
+ openedAt: z.ZodISODateTime;
224
+ closedAt: z.ZodISODateTime;
225
+ password: z.ZodOptional<z.ZodString>;
226
+ }, z.core.$strict>>;
227
+ problems: z.ZodArray<z.ZodObject<{
228
+ id: z.ZodString;
229
+ score: z.ZodNumber;
230
+ courseId: z.ZodOptional<z.ZodString>;
231
+ areTestCasesHidden: z.ZodOptional<z.ZodBoolean>;
232
+ isDebugHintDisabled: z.ZodOptional<z.ZodBoolean>;
233
+ isFixHintDisabled: z.ZodOptional<z.ZodBoolean>;
234
+ isDiffHintDisabled: z.ZodOptional<z.ZodBoolean>;
235
+ debugHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
236
+ fixHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
237
+ diffHintWaitingSeconds: z.ZodOptional<z.ZodNumber>;
238
+ isMaterialChatDisabled: z.ZodOptional<z.ZodBoolean>;
239
+ }, z.core.$strict>>;
240
+ }, z.core.$strict>;
241
+ export {};
@@ -0,0 +1,2 @@
1
+ import{languageIdToDefinition as e}from"../types/language.js";import{codeRuleSchema as t}from"../types/problem.js";import{z as n}from"zod";const r=/^[0-9_a-z-]+$/,i=n.string().regex(r),a=Object.keys(e),o=n.enum(a),s=n.string().regex(/^\w+$/),c=n.strictObject({name:n.string().min(1),type:n.enum([`prompt_study`]).optional(),timeLimitMs:n.number().int().min(0).optional(),memoryLimitByte:n.number().int().min(0).optional(),requiredRegExpsInCode:t.array().default([]),forbiddenRegExpsInCode:t.array().default([]),forbiddenTextsInCode:t.array().default([]),canCreateFiles:n.boolean().optional(),isEditorDisabled:n.boolean().optional(),isAttachedFileRequired:n.boolean().optional(),isManualScoringRequired:n.boolean().optional(),isVotable:n.boolean().optional(),requiredEnvironmentVariables:n.array(s).default([]),requiredOutputFilePaths:n.string().min(1).array().default([]),requiredSubmissionFilePaths:n.string().min(1).array().default([])}),l={availableLanguageIds:o.array().optional(),areTestCasesHidden:n.boolean().optional(),isProblemGradingResultHidden:n.boolean().optional(),isAutoFormatDisabled:n.boolean().optional(),isCopyAndPasteDisabled:n.boolean().optional(),isDebugHintDisabled:n.boolean().optional(),isFixHintDisabled:n.boolean().optional(),isDiffHintDisabled:n.boolean().optional(),debugHintWaitingSeconds:n.number().min(0).optional(),fixHintWaitingSeconds:n.number().min(0).optional(),diffHintWaitingSeconds:n.number().min(0).optional(),submissionOpenedAt:n.iso.datetime({offset:!0}).optional(),submissionSoftClosedAt:n.iso.datetime({offset:!0}).optional(),submissionHardClosedAt:n.iso.datetime({offset:!0}).optional(),isAutoTranslationDisabled:n.boolean().optional(),isModelAnswerShownAfterDeadline:n.boolean().optional(),isVotable:n.boolean().optional(),isMaterialChatDisabled:n.boolean().optional()},u=n.strictObject({...l,name:n.string().min(1),description:n.string(),author:n.string().min(1).optional(),isMotivationFeatureEnabled:n.boolean().optional(),isPublic:n.boolean().optional(),lectures:n.array(n.object({id:i,name:n.string().min(1),description:n.string()})).optional()}),d=n.string().array().min(1).refine(e=>e.length===new Set(e).size,{error:`options must contain unique values`}),f=n.string().regex(/^\d+$/,{error:`must be a non-negative integer index (quoted numbers such as "0" are accepted)`}).transform(Number),p={id:i,isResubmittable:n.boolean().optional(),isSurvey:n.boolean().optional(),explanation:n.string().optional(),hint:n.string().optional()},m={...p,type:n.literal(`select`),options:d,answerIndex:n.union([n.number().int().nonnegative(),f,n.number().int().nonnegative().array(),f.array()]).optional()},h={...p,type:n.literal(`select_multiple`),options:d,answerIndices:n.union([n.number().int().nonnegative().array(),f.array()]).optional()},g={...p,type:n.literal(`text`),answerPattern:n.string().min(1).optional(),modelAnswer:n.string().optional()},_=n.discriminatedUnion(`type`,[n.strictObject({...m,question:n.string()}),n.strictObject({...h,question:n.string()}),n.strictObject({...g,question:n.string()})]),v=n.discriminatedUnion(`type`,[n.strictObject({...m,question:n.string().optional()}),n.strictObject({...h,question:n.string().optional()}),n.strictObject({...g,question:n.string().optional()})]),y=n.strictObject({...l,name:n.string().min(1),problems:n.object({id:i,courseId:i.optional()}).array().optional(),questions:n.array(v).optional(),turtleGraphicsQuestions:n.object({id:i}).array().optional(),isExamination:n.boolean().optional(),isMockExamination:n.boolean().optional(),isRealtimeSurvey:n.boolean().optional()}),b=`.contest.yaml`,x=n.strictObject({id:i,name:n.string().min(1),openedAt:n.iso.datetime({offset:!0}),closedAt:n.iso.datetime({offset:!0}),password:n.string().min(1).optional()}).refine(e=>new Date(e.openedAt)<new Date(e.closedAt),{error:`openedAt must be earlier than closedAt`}),S=n.strictObject({id:i,score:n.number().int().nonnegative(),courseId:i.optional(),areTestCasesHidden:l.areTestCasesHidden,isDebugHintDisabled:l.isDebugHintDisabled,isFixHintDisabled:l.isFixHintDisabled,isDiffHintDisabled:l.isDiffHintDisabled,debugHintWaitingSeconds:l.debugHintWaitingSeconds,fixHintWaitingSeconds:l.fixHintWaitingSeconds,diffHintWaitingSeconds:l.diffHintWaitingSeconds,isMaterialChatDisabled:l.isMaterialChatDisabled}),C=n.strictObject({...l,name:n.string().min(1),description:n.string().optional(),adminEmails:n.array(n.string().trim().email()).optional(),showsProblemsAfterClose:n.boolean().optional(),divisions:n.array(x).min(1),problems:n.array(S).min(1)});export{b as CONTEST_MATERIAL_FILE_SUFFIX,r as LEARNING_MATERIAL_ID_REGEX,a as availableLanguageIds,C as contestFileSchema,u as courseFileSchema,i as learningMaterialIdSchema,y as materialFrontmatterSchema,c as problemFrontmatterSchema,_ as questionInCodeBlockSchema};
2
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","names":[],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { languageIdToDefinition } from '../types/language.js';\nimport { codeRuleSchema } from '../types/problem.js';\n\n// Schemas in this file mirror the Exercode judge importer so that generated materials validate\n// exactly like the importer would parse them.\n\nexport const LEARNING_MATERIAL_ID_REGEX = /^[0-9_a-z-]+$/;\n\nexport const learningMaterialIdSchema = z.string().regex(LEARNING_MATERIAL_ID_REGEX);\n\nexport const availableLanguageIds = Object.keys(languageIdToDefinition);\nconst availableLanguageIdSchema = z.enum(availableLanguageIds);\n\nconst environmentVariableNameSchema = z.string().regex(/^\\w+$/);\n\n// Mirrors the judge server's strict problem frontmatter schema; keys the server removed with the\n// v1 judge (judgeEnvironmentId, generalJudgeEnvironmentConfigOverrides, testCases, isGui) are\n// rejected here as unknown keys as well.\nexport const problemFrontmatterSchema = z.strictObject({\n name: z.string().min(1),\n type: z.enum(['prompt_study']).optional(),\n timeLimitMs: z.number().int().min(0).optional(),\n memoryLimitByte: z.number().int().min(0).optional(),\n requiredRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenTextsInCode: codeRuleSchema.array().default([]),\n canCreateFiles: z.boolean().optional(),\n isEditorDisabled: z.boolean().optional(),\n isAttachedFileRequired: z.boolean().optional(),\n isManualScoringRequired: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n requiredEnvironmentVariables: z.array(environmentVariableNameSchema).default([]),\n requiredOutputFilePaths: z.string().min(1).array().default([]),\n requiredSubmissionFilePaths: z.string().min(1).array().default([]),\n});\n\nexport type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;\n\nconst materialConfigShape = {\n availableLanguageIds: availableLanguageIdSchema.array().optional(),\n areTestCasesHidden: z.boolean().optional(),\n isProblemGradingResultHidden: z.boolean().optional(),\n isAutoFormatDisabled: z.boolean().optional(),\n isCopyAndPasteDisabled: z.boolean().optional(),\n isDebugHintDisabled: z.boolean().optional(),\n isFixHintDisabled: z.boolean().optional(),\n isDiffHintDisabled: z.boolean().optional(),\n debugHintWaitingSeconds: z.number().min(0).optional(),\n fixHintWaitingSeconds: z.number().min(0).optional(),\n diffHintWaitingSeconds: z.number().min(0).optional(),\n // z.iso.datetime is the zod v4 API (this repo pins zod 4); z.string().datetime is the v3 form.\n submissionOpenedAt: z.iso.datetime({ offset: true }).optional(),\n submissionSoftClosedAt: z.iso.datetime({ offset: true }).optional(),\n submissionHardClosedAt: z.iso.datetime({ offset: true }).optional(),\n isAutoTranslationDisabled: z.boolean().optional(),\n isModelAnswerShownAfterDeadline: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n isMaterialChatDisabled: z.boolean().optional(),\n} as const;\n\nexport const courseFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string(),\n author: z.string().min(1).optional(),\n isMotivationFeatureEnabled: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n // The importer strips unknown keys of a lecture entry (plain object) while the course object is strict.\n lectures: z\n .array(\n z.object({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n description: z.string(),\n })\n )\n .optional(),\n});\n\nexport type CourseFile = z.infer<typeof courseFileSchema>;\n\nconst uniqueOptionsSchema = z\n .string()\n .array()\n .min(1)\n // zod 4 names the custom-message option `error` (`message` is the deprecated zod 3 spelling).\n .refine((options) => options.length === new Set(options).size, { error: 'options must contain unique values' });\n\n// A quoted index is coerced like the judge does, but a non-numeric string is a schema error rather\n// than a NaN that would surface as an out-of-bounds index.\nconst answerIndexStringSchema = z\n .string()\n .regex(/^\\d+$/, { error: 'must be a non-negative integer index (quoted numbers such as \"0\" are accepted)' })\n .transform(Number);\n\nconst questionBaseShape = {\n id: learningMaterialIdSchema,\n isResubmittable: z.boolean().optional(),\n isSurvey: z.boolean().optional(),\n explanation: z.string().optional(),\n hint: z.string().optional(),\n} as const;\n\nconst selectQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select'),\n options: uniqueOptionsSchema,\n answerIndex: z\n .union([\n z.number().int().nonnegative(),\n answerIndexStringSchema,\n z.number().int().nonnegative().array(),\n answerIndexStringSchema.array(),\n ])\n .optional(),\n} as const;\n\nconst selectMultipleQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select_multiple'),\n options: uniqueOptionsSchema,\n answerIndices: z.union([z.number().int().nonnegative().array(), answerIndexStringSchema.array()]).optional(),\n} as const;\n\nconst textQuestionShape = {\n ...questionBaseShape,\n type: z.literal('text'),\n answerPattern: z.string().min(1).optional(),\n modelAnswer: z.string().optional(),\n} as const;\n\nexport const questionInCodeBlockSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string() }),\n z.strictObject({ ...textQuestionShape, question: z.string() }),\n]);\n\n// The judge still supports question definitions in material frontmatter where `question` is optional.\nconst questionInFrontmatterSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...textQuestionShape, question: z.string().optional() }),\n]);\n\nexport type MaterialQuestion = z.infer<typeof questionInFrontmatterSchema>;\n\nexport const materialFrontmatterSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n problems: z\n .object({ id: learningMaterialIdSchema, courseId: learningMaterialIdSchema.optional() })\n .array()\n .optional(),\n questions: z.array(questionInFrontmatterSchema).optional(),\n turtleGraphicsQuestions: z.object({ id: learningMaterialIdSchema }).array().optional(),\n isExamination: z.boolean().optional(),\n isMockExamination: z.boolean().optional(),\n isRealtimeSurvey: z.boolean().optional(),\n});\n\nexport const CONTEST_MATERIAL_FILE_SUFFIX = '.contest.yaml';\n\nconst contestDivisionSchema = z\n .strictObject({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n openedAt: z.iso.datetime({ offset: true }),\n closedAt: z.iso.datetime({ offset: true }),\n password: z.string().min(1).optional(),\n })\n .refine((division) => new Date(division.openedAt) < new Date(division.closedAt), {\n error: 'openedAt must be earlier than closedAt',\n });\n\nconst contestProblemSchema = z.strictObject({\n id: learningMaterialIdSchema,\n score: z.number().int().nonnegative(),\n courseId: learningMaterialIdSchema.optional(),\n // Per-problem overrides reuse the shared material-config fields, mirroring the judge schema.\n areTestCasesHidden: materialConfigShape.areTestCasesHidden,\n isDebugHintDisabled: materialConfigShape.isDebugHintDisabled,\n isFixHintDisabled: materialConfigShape.isFixHintDisabled,\n isDiffHintDisabled: materialConfigShape.isDiffHintDisabled,\n debugHintWaitingSeconds: materialConfigShape.debugHintWaitingSeconds,\n fixHintWaitingSeconds: materialConfigShape.fixHintWaitingSeconds,\n diffHintWaitingSeconds: materialConfigShape.diffHintWaitingSeconds,\n isMaterialChatDisabled: materialConfigShape.isMaterialChatDisabled,\n});\n\nexport const contestFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string().optional(),\n // Exercode reads the contest administrators itself and strips the key before the judge's strict schema.\n adminEmails: z.array(z.string().trim().email()).optional(),\n showsProblemsAfterClose: z.boolean().optional(),\n divisions: z.array(contestDivisionSchema).min(1),\n problems: z.array(contestProblemSchema).min(1),\n});\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("./schemas.cjs"),t=require("./fsHelpers.cjs"),n=require("./validationResult.cjs"),r=require("./validateMaterial.cjs");let i=require("node:path"),a=require("node:fs/promises"),o=require("yaml");async function s(s,c={}){let l=[],u=[],d={errors:l,warnings:u};if(!await t.isRegularFile(s))return l.push(await t.isFile(s)?`contest file is a symbolic link, which the judge ignores; commit a regular file: ${s}`:`contest file not found: ${s}`),d;let f=(0,i.basename)(s);if(!f.endsWith(`.contest.yaml`))return l.push(`contest file name must end with ${e.CONTEST_MATERIAL_FILE_SUFFIX}: ${f}`),d;let p=f.slice(0,-e.CONTEST_MATERIAL_FILE_SUFFIX.length);e.LEARNING_MATERIAL_ID_REGEX.test(p)||l.push(`contest ID "${p}" (file name without ${e.CONTEST_MATERIAL_FILE_SUFFIX}) must match ${e.LEARNING_MATERIAL_ID_REGEX}`);let m;try{m=(0,o.parse)(await(0,a.readFile)(s,`utf8`))}catch(e){return l.push(`invalid YAML: ${e instanceof Error?e.message:String(e)}`),d}let h=e.contestFileSchema.safeParse(m);if(!h.success)return l.push(...n.formatZodIssues(h.error,`contest`)),d;r.validateSubmissionPeriods(r.mergeSubmissionPeriods(h.data,c.courseSubmissionPeriods),l,h.data),r.reportForeignProblemCourseIds(h.data.problems,c.courseId,l),n.reportDuplicateIds(h.data.divisions.map(e=>e.id),`division`,l),n.reportDuplicateIds(h.data.problems.map(e=>e.id),`problem`,l);let g=c.availableProblemIds;return g===void 0&&(c.problemsDirectoryPath===void 0?c.isNestedInCourse||u.push(`no problems directory given (pass --problems-dir); problem references are not checked`):g=await t.collectProblemIdsFromDirectory(c.problemsDirectoryPath,l)),g!==void 0&&t.reportDanglingProblemReferences(h.data.problems.map(e=>e.id),g,c.problemsDirectoryPath,l),d}exports.validateContestFile=s;
2
+ //# sourceMappingURL=validateContest.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateContest.cjs","names":["isRegularFile","isFile","basename","CONTEST_MATERIAL_FILE_SUFFIX","LEARNING_MATERIAL_ID_REGEX","parseYaml","readFile","contestFileSchema","formatZodIssues","mergeSubmissionPeriods","collectProblemIdsFromDirectory"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport {\n mergeSubmissionPeriods,\n reportForeignProblemCourseIds,\n validateSubmissionPeriods,\n type SubmissionPeriods,\n} from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface ContestValidationOptions {\n /** When set, every contest problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n /** Set by course validation, which already warns once about a missing problems directory. */\n isNestedInCourse?: boolean;\n}\n\n/** Validates a judge contest material (`*.contest.yaml`) file. */\nexport async function validateContestFile(\n contestFilePath: string,\n options: ContestValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // The judge lists lecture entries as regular files, so a symbolic link is skipped at import time.\n if (!(await isRegularFile(contestFilePath))) {\n errors.push(\n (await isFile(contestFilePath))\n ? `contest file is a symbolic link, which the judge ignores; commit a regular file: ${contestFilePath}`\n : `contest file not found: ${contestFilePath}`\n );\n return result;\n }\n const fileName = basename(contestFilePath);\n if (!fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n errors.push(`contest file name must end with ${CONTEST_MATERIAL_FILE_SUFFIX}: ${fileName}`);\n return result;\n }\n const contestId = fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length);\n if (!LEARNING_MATERIAL_ID_REGEX.test(contestId)) {\n errors.push(\n `contest ID \"${contestId}\" (file name without ${CONTEST_MATERIAL_FILE_SUFFIX}) must match ${LEARNING_MATERIAL_ID_REGEX}`\n );\n }\n\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(contestFilePath, 'utf8'));\n } catch (error) {\n errors.push(`invalid YAML: ${error instanceof Error ? error.message : String(error)}`);\n return result;\n }\n const parsed = contestFileSchema.safeParse(rawContent);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'contest'));\n return result;\n }\n\n validateSubmissionPeriods(mergeSubmissionPeriods(parsed.data, options.courseSubmissionPeriods), errors, parsed.data);\n reportForeignProblemCourseIds(parsed.data.problems, options.courseId, errors);\n reportDuplicateIds(\n parsed.data.divisions.map((division) => division.id),\n 'division',\n errors\n );\n reportDuplicateIds(\n parsed.data.problems.map((problem) => problem.id),\n 'problem',\n errors\n );\n\n let availableProblemIds = options.availableProblemIds;\n if (availableProblemIds === undefined) {\n if (options.problemsDirectoryPath === undefined) {\n if (!options.isNestedInCourse) {\n warnings.push('no problems directory given (pass --problems-dir); problem references are not checked');\n }\n } else {\n availableProblemIds = await collectProblemIdsFromDirectory(options.problemsDirectoryPath, errors);\n }\n }\n if (availableProblemIds !== undefined) {\n reportDanglingProblemReferences(\n parsed.data.problems.map((problem) => problem.id),\n availableProblemIds,\n options.problemsDirectoryPath,\n errors\n );\n }\n return result;\n}\n"],"mappings":"8NA2BA,eAAsB,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAM,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACtB,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAMA,EAAAA,cAAc,CAAe,EAMvC,OALA,EAAO,KACJ,MAAMC,EAAAA,OAAO,CAAe,EACzB,oFAAoF,IACpF,2BAA2B,GACjC,EACO,EAET,IAAM,GAAA,EAAWC,EAAAA,SAAAA,CAAS,CAAe,EACzC,GAAI,CAAC,EAAS,SAAA,eAAqC,EAEjD,OADA,EAAO,KAAK,mCAAmCC,EAAAA,6BAA6B,IAAI,GAAU,EACnF,EAET,IAAM,EAAY,EAAS,MAAM,EAAG,CAACA,EAAAA,6BAA6B,MAAM,EACnEC,EAAAA,2BAA2B,KAAK,CAAS,GAC5C,EAAO,KACL,eAAe,EAAU,uBAAuBD,EAAAA,6BAA6B,eAAeC,EAAAA,4BAC9F,EAGF,IAAI,EACJ,GAAI,CACF,GAAA,EAAaC,EAAAA,MAAAA,CAAU,MAAA,EAAMC,EAAAA,SAAAA,CAAS,EAAiB,MAAM,CAAC,CAChE,OAAS,EAAO,CAEd,OADA,EAAO,KAAK,iBAAiB,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,EAC9E,CACT,CACA,IAAM,EAASC,EAAAA,kBAAkB,UAAU,CAAU,EACrD,GAAI,CAAC,EAAO,QAEV,OADA,EAAO,KAAK,GAAGC,EAAAA,gBAAgB,EAAO,MAAO,SAAS,CAAC,EAChD,EAGT,EAAA,0BAA0BC,EAAAA,uBAAuB,EAAO,KAAM,EAAQ,uBAAuB,EAAG,EAAQ,EAAO,IAAI,EACnH,EAAA,8BAA8B,EAAO,KAAK,SAAU,EAAQ,SAAU,CAAM,EAC5E,EAAA,mBACE,EAAO,KAAK,UAAU,IAAK,GAAa,EAAS,EAAE,EACnD,WACA,CACF,EACA,EAAA,mBACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,UACA,CACF,EAEA,IAAI,EAAsB,EAAQ,oBAkBlC,OAjBI,IAAwB,IAAA,KACtB,EAAQ,wBAA0B,IAAA,GAC/B,EAAQ,kBACX,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAMC,EAAAA,+BAA+B,EAAQ,sBAAuB,CAAM,GAGhG,IAAwB,IAAA,IAC1B,EAAA,gCACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,EACA,EAAQ,sBACR,CACF,EAEK,CACT"}
@@ -0,0 +1,16 @@
1
+ import { type SubmissionPeriods } from './validateMaterial.js';
2
+ import { type ValidationResult } from './validationResult.js';
3
+ export interface ContestValidationOptions {
4
+ /** When set, every contest problem ID must be a problem discovered under this path. */
5
+ problemsDirectoryPath?: string;
6
+ /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */
7
+ availableProblemIds?: ReadonlySet<string>;
8
+ /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */
9
+ courseId?: string;
10
+ /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */
11
+ courseSubmissionPeriods?: SubmissionPeriods;
12
+ /** Set by course validation, which already warns once about a missing problems directory. */
13
+ isNestedInCourse?: boolean;
14
+ }
15
+ /** Validates a judge contest material (`*.contest.yaml`) file. */
16
+ export declare function validateContestFile(contestFilePath: string, options?: ContestValidationOptions): Promise<ValidationResult>;
@@ -0,0 +1,2 @@
1
+ import{CONTEST_MATERIAL_FILE_SUFFIX as e,LEARNING_MATERIAL_ID_REGEX as t,contestFileSchema as n}from"./schemas.js";import{collectProblemIdsFromDirectory as r,isFile as i,isRegularFile as a,reportDanglingProblemReferences as o}from"./fsHelpers.js";import{formatZodIssues as s,reportDuplicateIds as c}from"./validationResult.js";import{mergeSubmissionPeriods as l,reportForeignProblemCourseIds as u,validateSubmissionPeriods as d}from"./validateMaterial.js";import{basename as f}from"node:path";import{readFile as p}from"node:fs/promises";import{parse as m}from"yaml";async function h(h,g={}){let _=[],v=[],y={errors:_,warnings:v};if(!await a(h))return _.push(await i(h)?`contest file is a symbolic link, which the judge ignores; commit a regular file: ${h}`:`contest file not found: ${h}`),y;let b=f(h);if(!b.endsWith(`.contest.yaml`))return _.push(`contest file name must end with ${e}: ${b}`),y;let x=b.slice(0,-e.length);t.test(x)||_.push(`contest ID "${x}" (file name without ${e}) must match ${t}`);let S;try{S=m(await p(h,`utf8`))}catch(e){return _.push(`invalid YAML: ${e instanceof Error?e.message:String(e)}`),y}let C=n.safeParse(S);if(!C.success)return _.push(...s(C.error,`contest`)),y;d(l(C.data,g.courseSubmissionPeriods),_,C.data),u(C.data.problems,g.courseId,_),c(C.data.divisions.map(e=>e.id),`division`,_),c(C.data.problems.map(e=>e.id),`problem`,_);let w=g.availableProblemIds;return w===void 0&&(g.problemsDirectoryPath===void 0?g.isNestedInCourse||v.push(`no problems directory given (pass --problems-dir); problem references are not checked`):w=await r(g.problemsDirectoryPath,_)),w!==void 0&&o(C.data.problems.map(e=>e.id),w,g.problemsDirectoryPath,_),y}export{h as validateContestFile};
2
+ //# sourceMappingURL=validateContest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateContest.js","names":["parseYaml"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport {\n mergeSubmissionPeriods,\n reportForeignProblemCourseIds,\n validateSubmissionPeriods,\n type SubmissionPeriods,\n} from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface ContestValidationOptions {\n /** When set, every contest problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n /** Set by course validation, which already warns once about a missing problems directory. */\n isNestedInCourse?: boolean;\n}\n\n/** Validates a judge contest material (`*.contest.yaml`) file. */\nexport async function validateContestFile(\n contestFilePath: string,\n options: ContestValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // The judge lists lecture entries as regular files, so a symbolic link is skipped at import time.\n if (!(await isRegularFile(contestFilePath))) {\n errors.push(\n (await isFile(contestFilePath))\n ? `contest file is a symbolic link, which the judge ignores; commit a regular file: ${contestFilePath}`\n : `contest file not found: ${contestFilePath}`\n );\n return result;\n }\n const fileName = basename(contestFilePath);\n if (!fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n errors.push(`contest file name must end with ${CONTEST_MATERIAL_FILE_SUFFIX}: ${fileName}`);\n return result;\n }\n const contestId = fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length);\n if (!LEARNING_MATERIAL_ID_REGEX.test(contestId)) {\n errors.push(\n `contest ID \"${contestId}\" (file name without ${CONTEST_MATERIAL_FILE_SUFFIX}) must match ${LEARNING_MATERIAL_ID_REGEX}`\n );\n }\n\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(contestFilePath, 'utf8'));\n } catch (error) {\n errors.push(`invalid YAML: ${error instanceof Error ? error.message : String(error)}`);\n return result;\n }\n const parsed = contestFileSchema.safeParse(rawContent);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, 'contest'));\n return result;\n }\n\n validateSubmissionPeriods(mergeSubmissionPeriods(parsed.data, options.courseSubmissionPeriods), errors, parsed.data);\n reportForeignProblemCourseIds(parsed.data.problems, options.courseId, errors);\n reportDuplicateIds(\n parsed.data.divisions.map((division) => division.id),\n 'division',\n errors\n );\n reportDuplicateIds(\n parsed.data.problems.map((problem) => problem.id),\n 'problem',\n errors\n );\n\n let availableProblemIds = options.availableProblemIds;\n if (availableProblemIds === undefined) {\n if (options.problemsDirectoryPath === undefined) {\n if (!options.isNestedInCourse) {\n warnings.push('no problems directory given (pass --problems-dir); problem references are not checked');\n }\n } else {\n availableProblemIds = await collectProblemIdsFromDirectory(options.problemsDirectoryPath, errors);\n }\n }\n if (availableProblemIds !== undefined) {\n reportDanglingProblemReferences(\n parsed.data.problems.map((problem) => problem.id),\n availableProblemIds,\n options.problemsDirectoryPath,\n errors\n );\n }\n return result;\n}\n"],"mappings":"sjBA2BA,eAAsB,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAM,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACtB,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAM,EAAc,CAAe,EAMvC,OALA,EAAO,KACJ,MAAM,EAAO,CAAe,EACzB,oFAAoF,IACpF,2BAA2B,GACjC,EACO,EAET,IAAM,EAAW,EAAS,CAAe,EACzC,GAAI,CAAC,EAAS,SAAA,eAAqC,EAEjD,OADA,EAAO,KAAK,mCAAmC,EAA6B,IAAI,GAAU,EACnF,EAET,IAAM,EAAY,EAAS,MAAM,EAAG,CAAC,EAA6B,MAAM,EACnE,EAA2B,KAAK,CAAS,GAC5C,EAAO,KACL,eAAe,EAAU,uBAAuB,EAA6B,eAAe,GAC9F,EAGF,IAAI,EACJ,GAAI,CACF,EAAaA,EAAU,MAAM,EAAS,EAAiB,MAAM,CAAC,CAChE,OAAS,EAAO,CAEd,OADA,EAAO,KAAK,iBAAiB,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,EAC9E,CACT,CACA,IAAM,EAAS,EAAkB,UAAU,CAAU,EACrD,GAAI,CAAC,EAAO,QAEV,OADA,EAAO,KAAK,GAAG,EAAgB,EAAO,MAAO,SAAS,CAAC,EAChD,EAGT,EAA0B,EAAuB,EAAO,KAAM,EAAQ,uBAAuB,EAAG,EAAQ,EAAO,IAAI,EACnH,EAA8B,EAAO,KAAK,SAAU,EAAQ,SAAU,CAAM,EAC5E,EACE,EAAO,KAAK,UAAU,IAAK,GAAa,EAAS,EAAE,EACnD,WACA,CACF,EACA,EACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,UACA,CACF,EAEA,IAAI,EAAsB,EAAQ,oBAkBlC,OAjBI,IAAwB,IAAA,KACtB,EAAQ,wBAA0B,IAAA,GAC/B,EAAQ,kBACX,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAM,EAA+B,EAAQ,sBAAuB,CAAM,GAGhG,IAAwB,IAAA,IAC1B,EACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,EACA,EAAQ,sBACR,CACF,EAEK,CACT"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("./schemas.cjs"),t=require("./fsHelpers.cjs"),n=require("./validationResult.cjs"),r=require("./validateMaterial.cjs"),i=require("./validateContest.cjs");let a=require("node:path"),o=require("node:fs/promises"),s=require("yaml");async function c(i,s={}){let c=[],f=[],p={errors:c,warnings:f};if(!await t.isRegularDirectory((0,a.resolve)(i)))return c.push(await t.isDirectory((0,a.resolve)(i))?`course directory is a symbolic link, which the judge does not traverse: ${i}`:`course directory not found: ${i}`),p;let h=await(0,o.realpath)(i),g=s.problemsDirectoryPath===void 0?void 0:await l(s.problemsDirectoryPath),v=(0,a.basename)(h);e.LEARNING_MATERIAL_ID_REGEX.test(v)||c.push(`course ID "${v}" (directory name) must match ${e.LEARNING_MATERIAL_ID_REGEX}`);let y=await d(h,c),b=h,x=await m(h,s.problemsDirectoryPath,g,c);if(!y)return p;let{courseFile:S,courseFileName:C}=y,w=[];r.validateSubmissionPeriods(S,w),c.push(...w.map(e=>`${C}: ${e}`));let T=S.lectures??[];T.length===0&&f.push(`${C} declares no lectures, so no materials are imported`),n.reportDuplicateIds(T.map(e=>e.id),`lecture`,c);for(let e of T)await _(h,{courseId:v,courseSubmissionPeriods:S,problemsDirectoryPath:b,availableProblemIds:x},e.id,c,f);return await u(h,S,C,f),p}async function l(e){let n=(0,a.resolve)(e);if(await t.isRegularDirectory(n))return(0,o.realpath)(n);try{return(0,a.join)(await(0,o.realpath)((0,a.dirname)(n)),(0,a.basename)(n))}catch{return n}}async function u(e,t,n,r){let i=new Set((t.lectures??[]).map(e=>e.id)),s=await(0,o.readdir)(e,{withFileTypes:!0});for(let t of s)!t.isDirectory()||t.name.startsWith(`.`)||i.has(t.name)||t.name===`problems`||(await(0,o.readdir)((0,a.join)(e,t.name),{withFileTypes:!0})).some(e=>e.isFile()&&p(e.name))&&r.push(`directory "${t.name}" is not listed as a lecture in ${n}, so its materials are not imported`)}async function d(r,i){let c=[`course.yaml`,`course.yml`],l=[];for(let e of c)await t.isRegularFile((0,a.join)(r,e))&&l.push(e);if(l.length>1){i.push(`both course.yaml and course.yml exist; the judge rejects the conflicting course files`);return}let u=l[0];if(u===void 0){let e=await f(c,e=>t.isFile((0,a.join)(r,e)));i.push(e===void 0?`course.yaml not found`:`${e} is a symbolic link, which the judge ignores; commit a regular file`);return}let d=(0,a.join)(r,u),p;try{p=(0,s.parse)(await(0,o.readFile)(d,`utf8`))}catch(e){i.push(`${u}: invalid YAML: ${e instanceof Error?e.message:String(e)}`);return}let m=e.courseFileSchema.safeParse(p);if(!m.success){i.push(...n.formatZodIssues(m.error,u));return}return{courseFile:m.data,courseFileName:u}}async function f(e,t){for(let n of e)if(await t(n))return n}function p(e){return e.endsWith(`.contest.yaml`)||e.endsWith(`.md`)&&e!==`problem.md`&&!e.endsWith(`.problem.md`)}async function m(e,n,r,i){let o=await t.collectProblemDefinitions(e);t.reportProblemDefinitionIssues(o,i);let s=new Set(o.keys()),c=(0,a.join)(e,`problems`),l=r!==void 0&&await h(r,c);return(l||await t.isDirectory(c))&&await t.isUsableProblemsDirectory(c,i),r===void 0||l||await t.isUsableProblemsDirectory(r,i)&&g(e,r)&&i.push(`problems directory ${n} is outside the course directory; Exercode links a material only to problems inside the course, so move them under the course`),s}async function h(e,t){if(e===t)return!0;try{let[n,r]=await Promise.all([(0,o.lstat)(e),(0,o.lstat)(t)]);return n.dev===r.dev&&n.ino===r.ino}catch{return!1}}function g(e,t){let n=(0,a.relative)(e,t);return n===`..`||n.startsWith(`..${a.sep}`)}async function _(s,c,l,u,d){let f=(0,a.join)(s,l);if(!await t.isRegularDirectory(f)){u.push(await t.isDirectory(f)?`lecture "${l}": directory is a symbolic link, which the judge does not traverse`:`lecture "${l}": directory not found`);return}let p=[],m=await(0,o.readdir)(f,{withFileTypes:!0});for(let e of m)e.isSymbolicLink()&&u.push(`${l}/${e.name} is a symbolic link, which the judge ignores; commit a regular file`);let h=m.filter(e=>e.isFile()).map(e=>e.name).toSorted((e,t)=>e.localeCompare(t));for(let t of h){let n=(0,a.join)(f,t),o;if(t.endsWith(`.contest.yaml`))p.push(t.slice(0,-e.CONTEST_MATERIAL_FILE_SUFFIX.length)),o=await i.validateContestFile(n,{...c,isNestedInCourse:!0});else if(t.endsWith(`.md`))p.push(t.replace(/\.[^/.]+$/,``)),o=await r.validateMaterialFile(n,c);else continue;u.push(...o.errors.map(e=>`${l}/${t}: ${e}`)),d.push(...o.warnings.map(e=>`${l}/${t}: ${e}`))}p.length===0&&d.push(`lecture "${l}": no material files (*.md or *${e.CONTEST_MATERIAL_FILE_SUFFIX}) found`);let g=[];n.reportDuplicateIds(p,`material`,g),u.push(...g.map(e=>`lecture "${l}": ${e}`))}exports.validateCourseDirectory=c;
2
+ //# sourceMappingURL=validateCourse.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateCourse.cjs","names":["isRegularDirectory","resolve","isDirectory","realpath","basename","LEARNING_MATERIAL_ID_REGEX","join","dirname","readdir","isRegularFile","isFile","parseYaml","readFile","courseFileSchema","formatZodIssues","collectProblemDefinitions","isUsableProblemsDirectory","lstat","relative","sep","CONTEST_MATERIAL_FILE_SUFFIX","validateContestFile","validateMaterialFile"],"sources":["../../src/learningMaterial/validateCourse.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath } from 'node:fs/promises';\nimport { basename, dirname, join, relative, resolve, sep } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport {\n collectProblemDefinitions,\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n isUsableProblemsDirectory,\n reportProblemDefinitionIssues,\n} from './fsHelpers.js';\nimport {\n CONTEST_MATERIAL_FILE_SUFFIX,\n courseFileSchema,\n LEARNING_MATERIAL_ID_REGEX,\n type CourseFile,\n} from './schemas.js';\nimport { validateContestFile } from './validateContest.js';\nimport { validateMaterialFile, validateSubmissionPeriods, type MaterialValidationOptions } from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface CourseValidationOptions {\n /**\n * Optional extra check only: problem references always resolve against the problems discovered\n * inside the course directory, so a given directory must lie inside it (an outside or missing one\n * is an error).\n */\n problemsDirectoryPath?: string;\n}\n\n/** Validates an exercode course directory (course.yaml plus one directory per lecture with material files). */\nexport async function validateCourseDirectory(\n courseDirectoryPath: string,\n options: CourseValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // Course discovery does not traverse a symbolic link, so a linked course directory is never imported.\n if (!(await isRegularDirectory(resolve(courseDirectoryPath)))) {\n errors.push(\n (await isDirectory(resolve(courseDirectoryPath)))\n ? `course directory is a symbolic link, which the judge does not traverse: ${courseDirectoryPath}`\n : `course directory not found: ${courseDirectoryPath}`\n );\n return result;\n }\n // Canonical paths make the \"inside the course\" comparisons below independent of how the caller\n // spelled either path (e.g. `/var` vs `/private/var` on macOS).\n const absoluteDirectoryPath = await realpath(courseDirectoryPath);\n const explicitProblemsDirectoryPath =\n options.problemsDirectoryPath === undefined ? undefined : await canonicalize(options.problemsDirectoryPath);\n const courseId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(courseId)) {\n errors.push(`course ID \"${courseId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n\n const parsedCourseFile = await parseCourseFile(absoluteDirectoryPath, errors);\n // Messages name the course directory because that is what discovery searches (see collectAvailableProblemIds).\n const problemsDirectoryPath = absoluteDirectoryPath;\n const availableProblemIds = await collectAvailableProblemIds(\n absoluteDirectoryPath,\n options.problemsDirectoryPath,\n explicitProblemsDirectoryPath,\n errors\n );\n\n if (!parsedCourseFile) return result;\n const { courseFile, courseFileName } = parsedCourseFile;\n const coursePeriodErrors: string[] = [];\n validateSubmissionPeriods(courseFile, coursePeriodErrors);\n errors.push(...coursePeriodErrors.map((error) => `${courseFileName}: ${error}`));\n // The importer treats a missing `lectures` as an empty list, so a course without lectures imports\n // nothing but is valid.\n const lectures = courseFile.lectures ?? [];\n if (lectures.length === 0) {\n warnings.push(`${courseFileName} declares no lectures, so no materials are imported`);\n }\n reportDuplicateIds(\n lectures.map((lecture) => lecture.id),\n 'lecture',\n errors\n );\n for (const lecture of lectures) {\n await validateLectureDirectory(\n absoluteDirectoryPath,\n { courseId, courseSubmissionPeriods: courseFile, problemsDirectoryPath, availableProblemIds },\n lecture.id,\n errors,\n warnings\n );\n }\n await reportOrphanLectureDirectories(absoluteDirectoryPath, courseFile, courseFileName, warnings);\n return result;\n}\n\n/**\n * Resolves symbolic links (and, on a case-insensitive file system, the casing) of a regular\n * directory; a linked or missing final entry keeps its own name so it can be reported as such,\n * with only its ancestors resolved.\n */\nasync function canonicalize(path: string): Promise<string> {\n const absolutePath = resolve(path);\n if (await isRegularDirectory(absolutePath)) return realpath(absolutePath);\n try {\n return join(await realpath(dirname(absolutePath)), basename(absolutePath));\n } catch {\n return absolutePath;\n }\n}\n\n/** Directories not listed as lectures are never imported, so their materials silently disappear. */\nasync function reportOrphanLectureDirectories(\n courseDirectoryPath: string,\n courseFile: CourseFile,\n courseFileName: string,\n warnings: string[]\n): Promise<void> {\n const lectureIds = new Set((courseFile.lectures ?? []).map((lecture) => lecture.id));\n const dirents = await readdir(courseDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents) {\n if (\n !dirent.isDirectory() ||\n dirent.name.startsWith('.') ||\n lectureIds.has(dirent.name) ||\n dirent.name === 'problems'\n )\n continue;\n // A lecture imports only its direct files, so only a directory that directly holds material files\n // would gain anything from being listed (problems are found at any depth regardless).\n const entries = await readdir(join(courseDirectoryPath, dirent.name), { withFileTypes: true });\n if (!entries.some((entry) => entry.isFile() && isMaterialFileName(entry.name))) continue;\n warnings.push(\n `directory \"${dirent.name}\" is not listed as a lecture in ${courseFileName}, so its materials are not imported`\n );\n }\n}\n\nasync function parseCourseFile(\n courseDirectoryPath: string,\n errors: string[]\n): Promise<{ courseFile: CourseFile; courseFileName: string } | undefined> {\n // The judge discovers courses by `course.yaml` or `course.yml` (rejecting a directory with both)\n // and skips symbolic links.\n const courseFileNames = ['course.yaml', 'course.yml'];\n const presentFileNames: string[] = [];\n for (const fileName of courseFileNames) {\n if (await isRegularFile(join(courseDirectoryPath, fileName))) presentFileNames.push(fileName);\n }\n if (presentFileNames.length > 1) {\n errors.push('both course.yaml and course.yml exist; the judge rejects the conflicting course files');\n return undefined;\n }\n const courseFileName = presentFileNames[0];\n if (courseFileName === undefined) {\n const symlinkFileName = await findFirst(courseFileNames, (fileName) => isFile(join(courseDirectoryPath, fileName)));\n errors.push(\n symlinkFileName === undefined\n ? 'course.yaml not found'\n : `${symlinkFileName} is a symbolic link, which the judge ignores; commit a regular file`\n );\n return undefined;\n }\n const courseFilePath = join(courseDirectoryPath, courseFileName);\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(courseFilePath, 'utf8'));\n } catch (error) {\n errors.push(`${courseFileName}: invalid YAML: ${error instanceof Error ? error.message : String(error)}`);\n return undefined;\n }\n const parsed = courseFileSchema.safeParse(rawContent);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, courseFileName));\n return undefined;\n }\n return { courseFile: parsed.data, courseFileName };\n}\n\nasync function findFirst<T>(items: readonly T[], predicate: (item: T) => Promise<boolean>): Promise<T | undefined> {\n for (const item of items) {\n if (await predicate(item)) return item;\n }\n return undefined;\n}\n\nfunction isMaterialFileName(fileName: string): boolean {\n return (\n fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX) ||\n (fileName.endsWith('.md') && fileName !== 'problem.md' && !fileName.endsWith('.problem.md'))\n );\n}\n\n/**\n * The judge scopes every problem inside the course directory (at any depth) to the course, so the\n * course is always searched; an explicit directory must lie inside it.\n */\nasync function collectAvailableProblemIds(\n courseDirectoryPath: string,\n problemsDirectoryOption: string | undefined,\n explicitProblemsDirectoryPath: string | undefined,\n errors: string[]\n): Promise<Set<string>> {\n const definitionPathsById = await collectProblemDefinitions(courseDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n const availableProblemIds = new Set(definitionPathsById.keys());\n // Discovery does not traverse a symbolic link, so a linked problems directory imports nothing.\n const courseProblemsDirectoryPath = join(courseDirectoryPath, 'problems');\n const namesConventionalDirectory =\n explicitProblemsDirectoryPath !== undefined &&\n (await isSameEntry(explicitProblemsDirectoryPath, courseProblemsDirectoryPath));\n if (namesConventionalDirectory || (await isDirectory(courseProblemsDirectoryPath))) {\n await isUsableProblemsDirectory(courseProblemsDirectoryPath, errors);\n }\n if (explicitProblemsDirectoryPath === undefined || namesConventionalDirectory) return availableProblemIds;\n if (\n (await isUsableProblemsDirectory(explicitProblemsDirectoryPath, errors)) &&\n isOutsideDirectory(courseDirectoryPath, explicitProblemsDirectoryPath)\n ) {\n // Exercode links a material only to problems inside its course, so outside problems never count.\n errors.push(\n `problems directory ${problemsDirectoryOption} is outside the course directory; Exercode links a material only to problems inside the course, so move them under the course`\n );\n }\n return availableProblemIds;\n}\n\n/** Whether two paths name the same directory entry (e.g. differently cased on a case-insensitive file system). */\nasync function isSameEntry(path1: string, path2: string): Promise<boolean> {\n if (path1 === path2) return true;\n try {\n const [stats1, stats2] = await Promise.all([lstat(path1), lstat(path2)]);\n return stats1.dev === stats2.dev && stats1.ino === stats2.ino;\n } catch {\n return false;\n }\n}\n\nfunction isOutsideDirectory(parentPath: string, path: string): boolean {\n const relativePath = relative(parentPath, path);\n return relativePath === '..' || relativePath.startsWith(`..${sep}`);\n}\n\nasync function validateLectureDirectory(\n courseDirectoryPath: string,\n materialOptions: MaterialValidationOptions,\n lectureId: string,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const lectureDirectoryPath = join(courseDirectoryPath, lectureId);\n // The judge lists lecture entries as regular files and directories, so symbolic links vanish.\n if (!(await isRegularDirectory(lectureDirectoryPath))) {\n errors.push(\n (await isDirectory(lectureDirectoryPath))\n ? `lecture \"${lectureId}\": directory is a symbolic link, which the judge does not traverse`\n : `lecture \"${lectureId}\": directory not found`\n );\n return;\n }\n\n const materialIds: string[] = [];\n const dirents = await readdir(lectureDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents) {\n if (dirent.isSymbolicLink()) {\n errors.push(`${lectureId}/${dirent.name} is a symbolic link, which the judge ignores; commit a regular file`);\n }\n }\n const fileNames = dirents\n .filter((dirent) => dirent.isFile())\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const materialFilePath = join(lectureDirectoryPath, fileName);\n let materialResult: ValidationResult;\n if (fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n materialIds.push(fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length));\n materialResult = await validateContestFile(materialFilePath, { ...materialOptions, isNestedInCourse: true });\n } else if (fileName.endsWith('.md')) {\n materialIds.push(fileName.replace(/\\.[^/.]+$/, ''));\n materialResult = await validateMaterialFile(materialFilePath, materialOptions);\n } else {\n continue;\n }\n errors.push(...materialResult.errors.map((error) => `${lectureId}/${fileName}: ${error}`));\n warnings.push(...materialResult.warnings.map((warning) => `${lectureId}/${fileName}: ${warning}`));\n }\n\n if (materialIds.length === 0) {\n warnings.push(`lecture \"${lectureId}\": no material files (*.md or *${CONTEST_MATERIAL_FILE_SUFFIX}) found`);\n }\n // Material IDs only need to be unique per lecture: exercode's import key is\n // `${courseId}.${lectureId}.${materialId}`, so a course-wide check would reject courses\n // that import fine. Scope the report to the lecture so multi-lecture courses point at the\n // right files.\n const duplicateMaterialErrors: string[] = [];\n reportDuplicateIds(materialIds, 'material', duplicateMaterialErrors);\n errors.push(...duplicateMaterialErrors.map((error) => `lecture \"${lectureId}\": ${error}`));\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { type ValidationResult } from './validationResult.js';
2
+ export interface CourseValidationOptions {
3
+ /**
4
+ * Optional extra check only: problem references always resolve against the problems discovered
5
+ * inside the course directory, so a given directory must lie inside it (an outside or missing one
6
+ * is an error).
7
+ */
8
+ problemsDirectoryPath?: string;
9
+ }
10
+ /** Validates an exercode course directory (course.yaml plus one directory per lecture with material files). */
11
+ export declare function validateCourseDirectory(courseDirectoryPath: string, options?: CourseValidationOptions): Promise<ValidationResult>;
@@ -0,0 +1,2 @@
1
+ import{CONTEST_MATERIAL_FILE_SUFFIX as e,LEARNING_MATERIAL_ID_REGEX as t,courseFileSchema as n}from"./schemas.js";import{collectProblemDefinitions as r,isDirectory as i,isFile as a,isRegularDirectory as o,isRegularFile as s,isUsableProblemsDirectory as c,reportProblemDefinitionIssues as l}from"./fsHelpers.js";import{formatZodIssues as u,reportDuplicateIds as d}from"./validationResult.js";import{validateMaterialFile as f,validateSubmissionPeriods as p}from"./validateMaterial.js";import{validateContestFile as m}from"./validateContest.js";import{basename as h,dirname as g,join as _,relative as v,resolve as y,sep as b}from"node:path";import{lstat as x,readFile as S,readdir as C,realpath as w}from"node:fs/promises";import{parse as T}from"yaml";async function E(e,n={}){let r=[],a=[],s={errors:r,warnings:a};if(!await o(y(e)))return r.push(await i(y(e))?`course directory is a symbolic link, which the judge does not traverse: ${e}`:`course directory not found: ${e}`),s;let c=await w(e),l=n.problemsDirectoryPath===void 0?void 0:await D(n.problemsDirectoryPath),u=h(c);t.test(u)||r.push(`course ID "${u}" (directory name) must match ${t}`);let f=await k(c,r),m=c,g=await M(c,n.problemsDirectoryPath,l,r);if(!f)return s;let{courseFile:_,courseFileName:v}=f,b=[];p(_,b),r.push(...b.map(e=>`${v}: ${e}`));let x=_.lectures??[];x.length===0&&a.push(`${v} declares no lectures, so no materials are imported`),d(x.map(e=>e.id),`lecture`,r);for(let e of x)await F(c,{courseId:u,courseSubmissionPeriods:_,problemsDirectoryPath:m,availableProblemIds:g},e.id,r,a);return await O(c,_,v,a),s}async function D(e){let t=y(e);if(await o(t))return w(t);try{return _(await w(g(t)),h(t))}catch{return t}}async function O(e,t,n,r){let i=new Set((t.lectures??[]).map(e=>e.id)),a=await C(e,{withFileTypes:!0});for(let t of a)!t.isDirectory()||t.name.startsWith(`.`)||i.has(t.name)||t.name===`problems`||(await C(_(e,t.name),{withFileTypes:!0})).some(e=>e.isFile()&&j(e.name))&&r.push(`directory "${t.name}" is not listed as a lecture in ${n}, so its materials are not imported`)}async function k(e,t){let r=[`course.yaml`,`course.yml`],i=[];for(let t of r)await s(_(e,t))&&i.push(t);if(i.length>1){t.push(`both course.yaml and course.yml exist; the judge rejects the conflicting course files`);return}let o=i[0];if(o===void 0){let n=await A(r,t=>a(_(e,t)));t.push(n===void 0?`course.yaml not found`:`${n} is a symbolic link, which the judge ignores; commit a regular file`);return}let c=_(e,o),l;try{l=T(await S(c,`utf8`))}catch(e){t.push(`${o}: invalid YAML: ${e instanceof Error?e.message:String(e)}`);return}let d=n.safeParse(l);if(!d.success){t.push(...u(d.error,o));return}return{courseFile:d.data,courseFileName:o}}async function A(e,t){for(let n of e)if(await t(n))return n}function j(e){return e.endsWith(`.contest.yaml`)||e.endsWith(`.md`)&&e!==`problem.md`&&!e.endsWith(`.problem.md`)}async function M(e,t,n,a){let o=await r(e);l(o,a);let s=new Set(o.keys()),u=_(e,`problems`),d=n!==void 0&&await N(n,u);return(d||await i(u))&&await c(u,a),n===void 0||d||await c(n,a)&&P(e,n)&&a.push(`problems directory ${t} is outside the course directory; Exercode links a material only to problems inside the course, so move them under the course`),s}async function N(e,t){if(e===t)return!0;try{let[n,r]=await Promise.all([x(e),x(t)]);return n.dev===r.dev&&n.ino===r.ino}catch{return!1}}function P(e,t){let n=v(e,t);return n===`..`||n.startsWith(`..${b}`)}async function F(t,n,r,a,s){let c=_(t,r);if(!await o(c)){a.push(await i(c)?`lecture "${r}": directory is a symbolic link, which the judge does not traverse`:`lecture "${r}": directory not found`);return}let l=[],u=await C(c,{withFileTypes:!0});for(let e of u)e.isSymbolicLink()&&a.push(`${r}/${e.name} is a symbolic link, which the judge ignores; commit a regular file`);let p=u.filter(e=>e.isFile()).map(e=>e.name).toSorted((e,t)=>e.localeCompare(t));for(let t of p){let i=_(c,t),o;if(t.endsWith(`.contest.yaml`))l.push(t.slice(0,-e.length)),o=await m(i,{...n,isNestedInCourse:!0});else if(t.endsWith(`.md`))l.push(t.replace(/\.[^/.]+$/,``)),o=await f(i,n);else continue;a.push(...o.errors.map(e=>`${r}/${t}: ${e}`)),s.push(...o.warnings.map(e=>`${r}/${t}: ${e}`))}l.length===0&&s.push(`lecture "${r}": no material files (*.md or *${e}) found`);let h=[];d(l,`material`,h),a.push(...h.map(e=>`lecture "${r}": ${e}`))}export{E as validateCourseDirectory};
2
+ //# sourceMappingURL=validateCourse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateCourse.js","names":["parseYaml"],"sources":["../../src/learningMaterial/validateCourse.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath } from 'node:fs/promises';\nimport { basename, dirname, join, relative, resolve, sep } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport {\n collectProblemDefinitions,\n isDirectory,\n isFile,\n isRegularDirectory,\n isRegularFile,\n isUsableProblemsDirectory,\n reportProblemDefinitionIssues,\n} from './fsHelpers.js';\nimport {\n CONTEST_MATERIAL_FILE_SUFFIX,\n courseFileSchema,\n LEARNING_MATERIAL_ID_REGEX,\n type CourseFile,\n} from './schemas.js';\nimport { validateContestFile } from './validateContest.js';\nimport { validateMaterialFile, validateSubmissionPeriods, type MaterialValidationOptions } from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface CourseValidationOptions {\n /**\n * Optional extra check only: problem references always resolve against the problems discovered\n * inside the course directory, so a given directory must lie inside it (an outside or missing one\n * is an error).\n */\n problemsDirectoryPath?: string;\n}\n\n/** Validates an exercode course directory (course.yaml plus one directory per lecture with material files). */\nexport async function validateCourseDirectory(\n courseDirectoryPath: string,\n options: CourseValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // Course discovery does not traverse a symbolic link, so a linked course directory is never imported.\n if (!(await isRegularDirectory(resolve(courseDirectoryPath)))) {\n errors.push(\n (await isDirectory(resolve(courseDirectoryPath)))\n ? `course directory is a symbolic link, which the judge does not traverse: ${courseDirectoryPath}`\n : `course directory not found: ${courseDirectoryPath}`\n );\n return result;\n }\n // Canonical paths make the \"inside the course\" comparisons below independent of how the caller\n // spelled either path (e.g. `/var` vs `/private/var` on macOS).\n const absoluteDirectoryPath = await realpath(courseDirectoryPath);\n const explicitProblemsDirectoryPath =\n options.problemsDirectoryPath === undefined ? undefined : await canonicalize(options.problemsDirectoryPath);\n const courseId = basename(absoluteDirectoryPath);\n if (!LEARNING_MATERIAL_ID_REGEX.test(courseId)) {\n errors.push(`course ID \"${courseId}\" (directory name) must match ${LEARNING_MATERIAL_ID_REGEX}`);\n }\n\n const parsedCourseFile = await parseCourseFile(absoluteDirectoryPath, errors);\n // Messages name the course directory because that is what discovery searches (see collectAvailableProblemIds).\n const problemsDirectoryPath = absoluteDirectoryPath;\n const availableProblemIds = await collectAvailableProblemIds(\n absoluteDirectoryPath,\n options.problemsDirectoryPath,\n explicitProblemsDirectoryPath,\n errors\n );\n\n if (!parsedCourseFile) return result;\n const { courseFile, courseFileName } = parsedCourseFile;\n const coursePeriodErrors: string[] = [];\n validateSubmissionPeriods(courseFile, coursePeriodErrors);\n errors.push(...coursePeriodErrors.map((error) => `${courseFileName}: ${error}`));\n // The importer treats a missing `lectures` as an empty list, so a course without lectures imports\n // nothing but is valid.\n const lectures = courseFile.lectures ?? [];\n if (lectures.length === 0) {\n warnings.push(`${courseFileName} declares no lectures, so no materials are imported`);\n }\n reportDuplicateIds(\n lectures.map((lecture) => lecture.id),\n 'lecture',\n errors\n );\n for (const lecture of lectures) {\n await validateLectureDirectory(\n absoluteDirectoryPath,\n { courseId, courseSubmissionPeriods: courseFile, problemsDirectoryPath, availableProblemIds },\n lecture.id,\n errors,\n warnings\n );\n }\n await reportOrphanLectureDirectories(absoluteDirectoryPath, courseFile, courseFileName, warnings);\n return result;\n}\n\n/**\n * Resolves symbolic links (and, on a case-insensitive file system, the casing) of a regular\n * directory; a linked or missing final entry keeps its own name so it can be reported as such,\n * with only its ancestors resolved.\n */\nasync function canonicalize(path: string): Promise<string> {\n const absolutePath = resolve(path);\n if (await isRegularDirectory(absolutePath)) return realpath(absolutePath);\n try {\n return join(await realpath(dirname(absolutePath)), basename(absolutePath));\n } catch {\n return absolutePath;\n }\n}\n\n/** Directories not listed as lectures are never imported, so their materials silently disappear. */\nasync function reportOrphanLectureDirectories(\n courseDirectoryPath: string,\n courseFile: CourseFile,\n courseFileName: string,\n warnings: string[]\n): Promise<void> {\n const lectureIds = new Set((courseFile.lectures ?? []).map((lecture) => lecture.id));\n const dirents = await readdir(courseDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents) {\n if (\n !dirent.isDirectory() ||\n dirent.name.startsWith('.') ||\n lectureIds.has(dirent.name) ||\n dirent.name === 'problems'\n )\n continue;\n // A lecture imports only its direct files, so only a directory that directly holds material files\n // would gain anything from being listed (problems are found at any depth regardless).\n const entries = await readdir(join(courseDirectoryPath, dirent.name), { withFileTypes: true });\n if (!entries.some((entry) => entry.isFile() && isMaterialFileName(entry.name))) continue;\n warnings.push(\n `directory \"${dirent.name}\" is not listed as a lecture in ${courseFileName}, so its materials are not imported`\n );\n }\n}\n\nasync function parseCourseFile(\n courseDirectoryPath: string,\n errors: string[]\n): Promise<{ courseFile: CourseFile; courseFileName: string } | undefined> {\n // The judge discovers courses by `course.yaml` or `course.yml` (rejecting a directory with both)\n // and skips symbolic links.\n const courseFileNames = ['course.yaml', 'course.yml'];\n const presentFileNames: string[] = [];\n for (const fileName of courseFileNames) {\n if (await isRegularFile(join(courseDirectoryPath, fileName))) presentFileNames.push(fileName);\n }\n if (presentFileNames.length > 1) {\n errors.push('both course.yaml and course.yml exist; the judge rejects the conflicting course files');\n return undefined;\n }\n const courseFileName = presentFileNames[0];\n if (courseFileName === undefined) {\n const symlinkFileName = await findFirst(courseFileNames, (fileName) => isFile(join(courseDirectoryPath, fileName)));\n errors.push(\n symlinkFileName === undefined\n ? 'course.yaml not found'\n : `${symlinkFileName} is a symbolic link, which the judge ignores; commit a regular file`\n );\n return undefined;\n }\n const courseFilePath = join(courseDirectoryPath, courseFileName);\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(courseFilePath, 'utf8'));\n } catch (error) {\n errors.push(`${courseFileName}: invalid YAML: ${error instanceof Error ? error.message : String(error)}`);\n return undefined;\n }\n const parsed = courseFileSchema.safeParse(rawContent);\n if (!parsed.success) {\n errors.push(...formatZodIssues(parsed.error, courseFileName));\n return undefined;\n }\n return { courseFile: parsed.data, courseFileName };\n}\n\nasync function findFirst<T>(items: readonly T[], predicate: (item: T) => Promise<boolean>): Promise<T | undefined> {\n for (const item of items) {\n if (await predicate(item)) return item;\n }\n return undefined;\n}\n\nfunction isMaterialFileName(fileName: string): boolean {\n return (\n fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX) ||\n (fileName.endsWith('.md') && fileName !== 'problem.md' && !fileName.endsWith('.problem.md'))\n );\n}\n\n/**\n * The judge scopes every problem inside the course directory (at any depth) to the course, so the\n * course is always searched; an explicit directory must lie inside it.\n */\nasync function collectAvailableProblemIds(\n courseDirectoryPath: string,\n problemsDirectoryOption: string | undefined,\n explicitProblemsDirectoryPath: string | undefined,\n errors: string[]\n): Promise<Set<string>> {\n const definitionPathsById = await collectProblemDefinitions(courseDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n const availableProblemIds = new Set(definitionPathsById.keys());\n // Discovery does not traverse a symbolic link, so a linked problems directory imports nothing.\n const courseProblemsDirectoryPath = join(courseDirectoryPath, 'problems');\n const namesConventionalDirectory =\n explicitProblemsDirectoryPath !== undefined &&\n (await isSameEntry(explicitProblemsDirectoryPath, courseProblemsDirectoryPath));\n if (namesConventionalDirectory || (await isDirectory(courseProblemsDirectoryPath))) {\n await isUsableProblemsDirectory(courseProblemsDirectoryPath, errors);\n }\n if (explicitProblemsDirectoryPath === undefined || namesConventionalDirectory) return availableProblemIds;\n if (\n (await isUsableProblemsDirectory(explicitProblemsDirectoryPath, errors)) &&\n isOutsideDirectory(courseDirectoryPath, explicitProblemsDirectoryPath)\n ) {\n // Exercode links a material only to problems inside its course, so outside problems never count.\n errors.push(\n `problems directory ${problemsDirectoryOption} is outside the course directory; Exercode links a material only to problems inside the course, so move them under the course`\n );\n }\n return availableProblemIds;\n}\n\n/** Whether two paths name the same directory entry (e.g. differently cased on a case-insensitive file system). */\nasync function isSameEntry(path1: string, path2: string): Promise<boolean> {\n if (path1 === path2) return true;\n try {\n const [stats1, stats2] = await Promise.all([lstat(path1), lstat(path2)]);\n return stats1.dev === stats2.dev && stats1.ino === stats2.ino;\n } catch {\n return false;\n }\n}\n\nfunction isOutsideDirectory(parentPath: string, path: string): boolean {\n const relativePath = relative(parentPath, path);\n return relativePath === '..' || relativePath.startsWith(`..${sep}`);\n}\n\nasync function validateLectureDirectory(\n courseDirectoryPath: string,\n materialOptions: MaterialValidationOptions,\n lectureId: string,\n errors: string[],\n warnings: string[]\n): Promise<void> {\n const lectureDirectoryPath = join(courseDirectoryPath, lectureId);\n // The judge lists lecture entries as regular files and directories, so symbolic links vanish.\n if (!(await isRegularDirectory(lectureDirectoryPath))) {\n errors.push(\n (await isDirectory(lectureDirectoryPath))\n ? `lecture \"${lectureId}\": directory is a symbolic link, which the judge does not traverse`\n : `lecture \"${lectureId}\": directory not found`\n );\n return;\n }\n\n const materialIds: string[] = [];\n const dirents = await readdir(lectureDirectoryPath, { withFileTypes: true });\n for (const dirent of dirents) {\n if (dirent.isSymbolicLink()) {\n errors.push(`${lectureId}/${dirent.name} is a symbolic link, which the judge ignores; commit a regular file`);\n }\n }\n const fileNames = dirents\n .filter((dirent) => dirent.isFile())\n .map((dirent) => dirent.name)\n .toSorted((f1, f2) => f1.localeCompare(f2));\n for (const fileName of fileNames) {\n const materialFilePath = join(lectureDirectoryPath, fileName);\n let materialResult: ValidationResult;\n if (fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n materialIds.push(fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length));\n materialResult = await validateContestFile(materialFilePath, { ...materialOptions, isNestedInCourse: true });\n } else if (fileName.endsWith('.md')) {\n materialIds.push(fileName.replace(/\\.[^/.]+$/, ''));\n materialResult = await validateMaterialFile(materialFilePath, materialOptions);\n } else {\n continue;\n }\n errors.push(...materialResult.errors.map((error) => `${lectureId}/${fileName}: ${error}`));\n warnings.push(...materialResult.warnings.map((warning) => `${lectureId}/${fileName}: ${warning}`));\n }\n\n if (materialIds.length === 0) {\n warnings.push(`lecture \"${lectureId}\": no material files (*.md or *${CONTEST_MATERIAL_FILE_SUFFIX}) found`);\n }\n // Material IDs only need to be unique per lecture: exercode's import key is\n // `${courseId}.${lectureId}.${materialId}`, so a course-wide check would reject courses\n // that import fine. Scope the report to the lecture so multi-lecture courses point at the\n // right files.\n const duplicateMaterialErrors: string[] = [];\n reportDuplicateIds(materialIds, 'material', duplicateMaterialErrors);\n errors.push(...duplicateMaterialErrors.map((error) => `lecture \"${lectureId}\": ${error}`));\n}\n"],"mappings":"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"}