@exercode/problem-utils 4.3.6 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.d.ts +2 -1
  4. package/dist/cli/checkAllProblems.js +1 -1
  5. package/dist/cli/checkAllProblems.js.map +1 -1
  6. package/dist/cli/exercodeProblem.cjs.map +1 -1
  7. package/dist/cli/exercodeProblem.js.map +1 -1
  8. package/dist/cli/runSingleHarness.cjs.map +1 -1
  9. package/dist/cli/runSingleHarness.js.map +1 -1
  10. package/dist/cli/validateLearningMaterial.cjs.map +1 -1
  11. package/dist/cli/validateLearningMaterial.js.map +1 -1
  12. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  13. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  14. package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
  15. package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
  16. package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
  17. package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
  18. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs.map +1 -1
  19. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js.map +1 -1
  20. package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
  21. package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
  22. package/dist/helpers/defaultStdioHarness.cjs.map +1 -1
  23. package/dist/helpers/defaultStdioHarness.js.map +1 -1
  24. package/dist/helpers/findEntryPointFile.cjs.map +1 -1
  25. package/dist/helpers/findEntryPointFile.js.map +1 -1
  26. package/dist/helpers/findLanguageDefinitionByPath.cjs.map +1 -1
  27. package/dist/helpers/findLanguageDefinitionByPath.js.map +1 -1
  28. package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
  29. package/dist/helpers/findModelAnswerDirs.js.map +1 -1
  30. package/dist/helpers/guiRecording.cjs.map +1 -1
  31. package/dist/helpers/guiRecording.js.map +1 -1
  32. package/dist/helpers/javaJudge.cjs.map +1 -1
  33. package/dist/helpers/javaJudge.js.map +1 -1
  34. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  35. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  36. package/dist/helpers/parseArgs.cjs.map +1 -1
  37. package/dist/helpers/parseArgs.js.map +1 -1
  38. package/dist/helpers/parseCsv.cjs.map +1 -1
  39. package/dist/helpers/parseCsv.js.map +1 -1
  40. package/dist/helpers/printDebugBanner.cjs.map +1 -1
  41. package/dist/helpers/printDebugBanner.js.map +1 -1
  42. package/dist/helpers/printTestCaseResult.cjs.map +1 -1
  43. package/dist/helpers/printTestCaseResult.js.map +1 -1
  44. package/dist/helpers/readOutputFiles.cjs.map +1 -1
  45. package/dist/helpers/readOutputFiles.js.map +1 -1
  46. package/dist/helpers/readProblemMarkdownFrontMatter.cjs.map +1 -1
  47. package/dist/helpers/readProblemMarkdownFrontMatter.js.map +1 -1
  48. package/dist/helpers/readTestCases.cjs.map +1 -1
  49. package/dist/helpers/readTestCases.js.map +1 -1
  50. package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
  51. package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
  52. package/dist/helpers/resolveCwds.cjs.map +1 -1
  53. package/dist/helpers/resolveCwds.js.map +1 -1
  54. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  55. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  56. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  57. package/dist/helpers/runHarnessProcess.js.map +1 -1
  58. package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -1
  59. package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -1
  60. package/dist/helpers/runPythonInTemporaryUvProject.cjs.map +1 -1
  61. package/dist/helpers/runPythonInTemporaryUvProject.js.map +1 -1
  62. package/dist/helpers/sourceCodeGrammars.cjs.map +1 -1
  63. package/dist/helpers/sourceCodeGrammars.js.map +1 -1
  64. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  65. package/dist/helpers/spawnWithLimits.js.map +1 -1
  66. package/dist/helpers/spawnWithTimeout.cjs.map +1 -1
  67. package/dist/helpers/spawnWithTimeout.js.map +1 -1
  68. package/dist/helpers/startHttpServer.cjs.map +1 -1
  69. package/dist/helpers/startHttpServer.js.map +1 -1
  70. package/dist/helpers/stdioJudgeRules.cjs.map +1 -1
  71. package/dist/helpers/stdioJudgeRules.js.map +1 -1
  72. package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -1
  73. package/dist/helpers/temporaryProblemDirCopy.js.map +1 -1
  74. package/dist/index.cjs +1 -1
  75. package/dist/index.js +1 -1
  76. package/dist/learningMaterial/frontmatter.cjs.map +1 -1
  77. package/dist/learningMaterial/frontmatter.js.map +1 -1
  78. package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
  79. package/dist/learningMaterial/fsHelpers.js.map +1 -1
  80. package/dist/learningMaterial/schemas.cjs +1 -1
  81. package/dist/learningMaterial/schemas.cjs.map +1 -1
  82. package/dist/learningMaterial/schemas.d.ts +43 -3
  83. package/dist/learningMaterial/schemas.js +1 -1
  84. package/dist/learningMaterial/schemas.js.map +1 -1
  85. package/dist/learningMaterial/validateContest.cjs +1 -1
  86. package/dist/learningMaterial/validateContest.cjs.map +1 -1
  87. package/dist/learningMaterial/validateContest.d.ts +0 -2
  88. package/dist/learningMaterial/validateContest.js +1 -1
  89. package/dist/learningMaterial/validateContest.js.map +1 -1
  90. package/dist/learningMaterial/validateCourse.cjs +1 -1
  91. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  92. package/dist/learningMaterial/validateCourse.js +1 -1
  93. package/dist/learningMaterial/validateCourse.js.map +1 -1
  94. package/dist/learningMaterial/validateMaterial.cjs +3 -3
  95. package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
  96. package/dist/learningMaterial/validateMaterial.d.ts +0 -7
  97. package/dist/learningMaterial/validateMaterial.js +3 -3
  98. package/dist/learningMaterial/validateMaterial.js.map +1 -1
  99. package/dist/learningMaterial/validateProblem.cjs +1 -1
  100. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  101. package/dist/learningMaterial/validateProblem.js +1 -1
  102. package/dist/learningMaterial/validateProblem.js.map +1 -1
  103. package/dist/learningMaterial/validationResult.cjs.map +1 -1
  104. package/dist/learningMaterial/validationResult.js.map +1 -1
  105. package/dist/presets/command.cjs.map +1 -1
  106. package/dist/presets/command.js.map +1 -1
  107. package/dist/presets/evaluation.cjs.map +1 -1
  108. package/dist/presets/evaluation.js.map +1 -1
  109. package/dist/presets/guiCommand.cjs.map +1 -1
  110. package/dist/presets/guiCommand.js.map +1 -1
  111. package/dist/presets/springBoot.cjs.map +1 -1
  112. package/dist/presets/springBoot.js.map +1 -1
  113. package/dist/presets/stdio.cjs.map +1 -1
  114. package/dist/presets/stdio.js.map +1 -1
  115. package/dist/presets/tomcat.cjs.map +1 -1
  116. package/dist/presets/tomcat.js.map +1 -1
  117. package/dist/types/decisionCode.cjs.map +1 -1
  118. package/dist/types/decisionCode.js.map +1 -1
  119. package/dist/types/language.cjs.map +1 -1
  120. package/dist/types/language.js.map +1 -1
  121. package/dist/types/problem.cjs +1 -1
  122. package/dist/types/problem.cjs.map +1 -1
  123. package/dist/types/problem.d.ts +1 -0
  124. package/dist/types/problem.js +1 -1
  125. package/dist/types/problem.js.map +1 -1
  126. package/dist/types/testCaseResult.cjs.map +1 -1
  127. package/dist/types/testCaseResult.js.map +1 -1
  128. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.cjs","names":["z","languageIdToDefinition","codeRuleSchema","require_language","require_problem","zod","LEARNING_MATERIAL_ID_REGEX","learningMaterialIdSchema","availableLanguageIds","availableLanguageIdSchema","environmentVariableNameSchema","problemFrontmatterSchema","materialConfigShape","courseFileSchema","uniqueOptionsSchema","options","answerIndexStringSchema","questionBaseShape","selectQuestionShape","selectMultipleQuestionShape","textQuestionShape","questionInCodeBlockSchema","questionInFrontmatterSchema","materialFrontmatterSchema","contestDivisionSchema","division","contestProblemSchema","contestFileSchema"],"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":"6GAQA,MAAaM,EAA6B,gBAE7BC,EAA2BP,EAAAA,EAAE,OAAO,CAAC,CAAC,MAAMM,CAA0B,EAEtEE,EAAuB,OAAO,KAAKP,EAAAA,sBAAsB,EAChEQ,EAA4BT,EAAAA,EAAE,KAAKQ,CAAoB,EAEvDE,EAAgCV,EAAAA,EAAE,OAAO,CAAC,CAAC,MAAM,OAAO,EAKjDW,EAA2BX,EAAAA,EAAE,aAAa,CACrD,KAAMA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EACtB,KAAMA,EAAAA,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,EACxC,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAC9C,gBAAiBA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAClD,sBAAuBE,EAAAA,eAAe,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EACxD,uBAAwBA,EAAAA,eAAe,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EACzD,qBAAsBA,EAAAA,eAAe,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EACvD,eAAgBF,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACrC,iBAAkBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACvC,uBAAwBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC7C,wBAAyBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC9C,UAAWA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAChC,6BAA8BA,EAAAA,EAAE,MAAMU,CAA6B,CAAC,CAAC,QAAQ,CAAC,CAAC,EAC/E,wBAAyBV,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAC7D,4BAA6BA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CACnE,CAAC,EAIKY,EAAsB,CAC1B,qBAAsBH,EAA0B,MAAM,CAAC,CAAC,SAAS,EACjE,mBAAoBT,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACzC,6BAA8BA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACnD,qBAAsBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC3C,uBAAwBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC7C,oBAAqBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC1C,kBAAmBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACxC,mBAAoBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACzC,wBAAyBA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EACpD,sBAAuBA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAClD,uBAAwBA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAEnD,mBAAoBA,EAAAA,EAAE,IAAI,SAAS,CAAE,OAAQ,EAAK,CAAC,CAAC,CAAC,SAAS,EAC9D,uBAAwBA,EAAAA,EAAE,IAAI,SAAS,CAAE,OAAQ,EAAK,CAAC,CAAC,CAAC,SAAS,EAClE,uBAAwBA,EAAAA,EAAE,IAAI,SAAS,CAAE,OAAQ,EAAK,CAAC,CAAC,CAAC,SAAS,EAClE,0BAA2BA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAChD,gCAAiCA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACtD,UAAWA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAChC,uBAAwBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,CAC/C,EAEaa,EAAmBb,EAAAA,EAAE,aAAa,CAC7C,GAAGY,EACH,KAAMZ,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EACtB,YAAaA,EAAAA,EAAE,OAAO,EACtB,OAAQA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EACnC,2BAA4BA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACjD,SAAUA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAE/B,SAAUA,EAAAA,EACP,MACCA,EAAAA,EAAE,OAAO,CACP,GAAIO,EACJ,KAAMP,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EACtB,YAAaA,EAAAA,EAAE,OAAO,CACxB,CAAC,CACH,CAAC,CACA,SAAS,CACd,CAAC,EAIKc,EAAsBd,EAAAA,EACzB,OAAO,CAAC,CACR,MAAM,CAAC,CACP,IAAI,CAAC,CAAC,CAEN,OAAQ,GAAYe,EAAQ,SAAW,IAAI,IAAIA,CAAO,CAAC,CAAC,KAAM,CAAE,MAAO,oCAAqC,CAAC,EAI1GC,EAA0BhB,EAAAA,EAC7B,OAAO,CAAC,CACR,MAAM,QAAS,CAAE,MAAO,gFAAiF,CAAC,CAAC,CAC3G,UAAU,MAAM,EAEbiB,EAAoB,CACxB,GAAIV,EACJ,gBAAiBP,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACtC,SAAUA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC/B,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,KAAMA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,CAC5B,EAEMkB,EAAsB,CAC1B,GAAGD,EACH,KAAMjB,EAAAA,EAAE,QAAQ,QAAQ,EACxB,QAASc,EACT,YAAad,EAAAA,EACV,MAAM,CACLA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAC7BgB,EACAhB,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,EACrCgB,EAAwB,MAAM,CAChC,CAAC,CAAC,CACD,SAAS,CACd,EAEMG,EAA8B,CAClC,GAAGF,EACH,KAAMjB,EAAAA,EAAE,QAAQ,iBAAiB,EACjC,QAASc,EACT,cAAed,EAAAA,EAAE,MAAM,CAACA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM,EAAGgB,EAAwB,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAC7G,EAEMI,EAAoB,CACxB,GAAGH,EACH,KAAMjB,EAAAA,EAAE,QAAQ,MAAM,EACtB,cAAeA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAC1C,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,CACnC,EAEaqB,EAA4BrB,EAAAA,EAAE,mBAAmB,OAAQ,CACpEA,EAAAA,EAAE,aAAa,CAAE,GAAGkB,EAAqB,SAAUlB,EAAAA,EAAE,OAAO,CAAE,CAAC,EAC/DA,EAAAA,EAAE,aAAa,CAAE,GAAGmB,EAA6B,SAAUnB,EAAAA,EAAE,OAAO,CAAE,CAAC,EACvEA,EAAAA,EAAE,aAAa,CAAE,GAAGoB,EAAmB,SAAUpB,EAAAA,EAAE,OAAO,CAAE,CAAC,CAC/D,CAAC,EAGKsB,EAA8BtB,EAAAA,EAAE,mBAAmB,OAAQ,CAC/DA,EAAAA,EAAE,aAAa,CAAE,GAAGkB,EAAqB,SAAUlB,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,CAAE,CAAC,EAC1EA,EAAAA,EAAE,aAAa,CAAE,GAAGmB,EAA6B,SAAUnB,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,CAAE,CAAC,EAClFA,EAAAA,EAAE,aAAa,CAAE,GAAGoB,EAAmB,SAAUpB,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,CAAE,CAAC,CAC1E,CAAC,EAIYuB,EAA4BvB,EAAAA,EAAE,aAAa,CACtD,GAAGY,EACH,KAAMZ,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EACtB,SAAUA,EAAAA,EACP,OAAO,CAAE,GAAIO,EAA0B,SAAUA,EAAyB,SAAS,CAAE,CAAC,CAAC,CACvF,MAAM,CAAC,CACP,SAAS,EACZ,UAAWP,EAAAA,EAAE,MAAMsB,CAA2B,CAAC,CAAC,SAAS,EACzD,wBAAyBtB,EAAAA,EAAE,OAAO,CAAE,GAAIO,CAAyB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,EACrF,cAAeP,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACpC,kBAAmBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EACxC,iBAAkBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EAIKwB,EAAwBxB,EAAAA,EAC3B,aAAa,CACZ,GAAIO,EACJ,KAAMP,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EACtB,SAAUA,EAAAA,EAAE,IAAI,SAAS,CAAE,OAAQ,EAAK,CAAC,EACzC,SAAUA,EAAAA,EAAE,IAAI,SAAS,CAAE,OAAQ,EAAK,CAAC,EACzC,SAAUA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CACvC,CAAC,CAAC,CACD,OAAQ,GAAa,IAAI,KAAKyB,EAAS,QAAQ,EAAI,IAAI,KAAKA,EAAS,QAAQ,EAAG,CAC/E,MAAO,wCACT,CAAC,EAEGC,EAAuB1B,EAAAA,EAAE,aAAa,CAC1C,GAAIO,EACJ,MAAOP,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EACpC,SAAUO,EAAyB,SAAS,EAE5C,mBAAoBK,EAAoB,mBACxC,oBAAqBA,EAAoB,oBACzC,kBAAmBA,EAAoB,kBACvC,mBAAoBA,EAAoB,mBACxC,wBAAyBA,EAAoB,wBAC7C,sBAAuBA,EAAoB,sBAC3C,uBAAwBA,EAAoB,uBAC5C,uBAAwBA,EAAoB,sBAC9C,CAAC,EAEYe,EAAoB3B,EAAAA,EAAE,aAAa,CAC9C,GAAGY,EACH,KAAMZ,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EACtB,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAEjC,YAAaA,EAAAA,EAAE,MAAMA,EAAAA,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EACzD,wBAAyBA,EAAAA,EAAE,QAAQ,CAAC,CAAC,SAAS,EAC9C,UAAWA,EAAAA,EAAE,MAAMwB,CAAqB,CAAC,CAAC,IAAI,CAAC,EAC/C,SAAUxB,EAAAA,EAAE,MAAM0B,CAAoB,CAAC,CAAC,IAAI,CAAC,CAC/C,CAAC"}
1
+ {"version":3,"file":"schemas.cjs","names":["z","languageIdToDefinition","codeRuleSchema","isRecord","require_language","require_problem","zod","_willbooster_shared_lib","LEARNING_MATERIAL_ID_REGEX","learningMaterialIdSchema","availableLanguageIds","availableLanguageIdSchema","environmentVariableNameSchema","jsonSchemaSchema","httpStatusSchema","pathTemplateSchema","path","segment","headersJsonSchemaSchema","declaresOnlyLowerCaseNames","schema","agentChallengeSchema","problemFrontmatterSchema","materialConfigShape","courseFileSchema","uniqueOptionsSchema","options","answerIndexStringSchema","questionBaseShape","selectQuestionShape","selectMultipleQuestionShape","textQuestionShape","questionInCodeBlockSchema","questionInFrontmatterSchema","materialFrontmatterSchema","contestDivisionSchema","division","contestProblemSchema","contestFileSchema","visited","item","propertyNames","requiredNames","name","value"],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { isRecord } from '@willbooster/shared-lib';\nimport { 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\nconst jsonSchemaSchema = z.record(z.string(), z.unknown());\nconst httpStatusSchema = z.number().int().min(100).max(599);\nconst pathTemplateSchema = z\n .string()\n .startsWith('/')\n .refine((path) => !path.includes('?') && !path.includes('#'), 'path must not carry a query or fragment; use query')\n .refine(\n (path) => path.split('/').every((segment) => !/[{}]/.test(segment) || /^\\{[^{}]+\\}$/.test(segment)),\n 'a {name} placeholder must be one whole path segment'\n );\n// Header names are lower-cased before matching, so a capitalized name could never match.\nconst headersJsonSchemaSchema = jsonSchemaSchema.refine(\n (schema) => declaresOnlyLowerCaseNames(schema),\n 'header names in properties and required must be lower case'\n);\n\n// Mirrors the judge server's `agentChallengeSchema`.\nexport const agentChallengeSchema = z.strictObject({\n playgrounds: z.array(z.enum(['bbs', 'backlog', 'hosting'])).min(1),\n expectedRequest: z.strictObject({\n method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),\n path: pathTemplateSchema,\n status: z.union([httpStatusSchema, z.array(httpStatusSchema).min(1)]).optional(),\n query: jsonSchemaSchema.optional(),\n headers: headersJsonSchemaSchema.optional(),\n body: jsonSchemaSchema.optional(),\n }),\n});\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 agentChallenge: agentChallengeSchema.optional(),\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.strictObject({ id: learningMaterialIdSchema }).array().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 // 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\n/** Checks every `properties` key and `required` item at any depth, so composed schemas (allOf, $defs, if/then) count too. */\nfunction declaresOnlyLowerCaseNames(schema: unknown, visited = new WeakSet<object>()): boolean {\n // A YAML alias can make the schema cyclic.\n if (typeof schema !== 'object' || schema === null || visited.has(schema)) return true;\n visited.add(schema);\n if (Array.isArray(schema)) return schema.every((item) => declaresOnlyLowerCaseNames(item, visited));\n if (!isRecord(schema)) return true;\n const propertyNames = isRecord(schema.properties) ? Object.keys(schema.properties) : [];\n const requiredNames: unknown[] = Array.isArray(schema.required) ? schema.required : [];\n return (\n [...propertyNames, ...requiredNames].every((name) => typeof name !== 'string' || name === name.toLowerCase()) &&\n Object.values(schema).every((value) => declaresOnlyLowerCaseNames(value, visited))\n );\n}\n"],"mappings":"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"}
@@ -2,6 +2,27 @@ import { z } from 'zod';
2
2
  export declare const LEARNING_MATERIAL_ID_REGEX: RegExp;
3
3
  export declare const learningMaterialIdSchema: z.ZodString;
4
4
  export declare const availableLanguageIds: string[];
5
+ export declare const agentChallengeSchema: z.ZodObject<{
6
+ playgrounds: z.ZodArray<z.ZodEnum<{
7
+ backlog: "backlog";
8
+ bbs: "bbs";
9
+ hosting: "hosting";
10
+ }>>;
11
+ expectedRequest: z.ZodObject<{
12
+ method: z.ZodEnum<{
13
+ DELETE: "DELETE";
14
+ GET: "GET";
15
+ PATCH: "PATCH";
16
+ POST: "POST";
17
+ PUT: "PUT";
18
+ }>;
19
+ path: z.ZodString;
20
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodArray<z.ZodNumber>]>>;
21
+ query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
22
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
23
+ body: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
24
+ }, z.core.$strict>;
25
+ }, z.core.$strict>;
5
26
  export declare const problemFrontmatterSchema: z.ZodObject<{
6
27
  name: z.ZodString;
7
28
  type: z.ZodOptional<z.ZodEnum<{
@@ -29,6 +50,27 @@ export declare const problemFrontmatterSchema: z.ZodObject<{
29
50
  requiredEnvironmentVariables: z.ZodDefault<z.ZodArray<z.ZodString>>;
30
51
  requiredOutputFilePaths: z.ZodDefault<z.ZodArray<z.ZodString>>;
31
52
  requiredSubmissionFilePaths: z.ZodDefault<z.ZodArray<z.ZodString>>;
53
+ agentChallenge: z.ZodOptional<z.ZodObject<{
54
+ playgrounds: z.ZodArray<z.ZodEnum<{
55
+ backlog: "backlog";
56
+ bbs: "bbs";
57
+ hosting: "hosting";
58
+ }>>;
59
+ expectedRequest: z.ZodObject<{
60
+ method: z.ZodEnum<{
61
+ DELETE: "DELETE";
62
+ GET: "GET";
63
+ PATCH: "PATCH";
64
+ POST: "POST";
65
+ PUT: "PUT";
66
+ }>;
67
+ path: z.ZodString;
68
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodArray<z.ZodNumber>]>>;
69
+ query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
70
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
71
+ body: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
72
+ }, z.core.$strict>;
73
+ }, z.core.$strict>>;
32
74
  }, z.core.$strict>;
33
75
  export type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;
34
76
  export declare const courseFileSchema: z.ZodObject<{
@@ -151,8 +193,7 @@ export declare const materialFrontmatterSchema: z.ZodObject<{
151
193
  name: z.ZodString;
152
194
  problems: z.ZodOptional<z.ZodArray<z.ZodObject<{
153
195
  id: z.ZodString;
154
- courseId: z.ZodOptional<z.ZodString>;
155
- }, z.core.$strip>>>;
196
+ }, z.core.$strict>>>;
156
197
  questions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
157
198
  id: z.ZodString;
158
199
  isResubmittable: z.ZodOptional<z.ZodBoolean>;
@@ -227,7 +268,6 @@ export declare const contestFileSchema: z.ZodObject<{
227
268
  problems: z.ZodArray<z.ZodObject<{
228
269
  id: z.ZodString;
229
270
  score: z.ZodNumber;
230
- courseId: z.ZodOptional<z.ZodString>;
231
271
  areTestCasesHidden: z.ZodOptional<z.ZodBoolean>;
232
272
  isDebugHintDisabled: z.ZodOptional<z.ZodBoolean>;
233
273
  isFixHintDisabled: z.ZodOptional<z.ZodBoolean>;
@@ -1,2 +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};
1
+ import{languageIdToDefinition as e}from"../types/language.js";import{codeRuleSchema as t}from"../types/problem.js";import{z as n}from"zod";import{isRecord as r}from"@willbooster/shared-lib";const i=/^[0-9_a-z-]+$/,a=n.string().regex(i),o=Object.keys(e),s=n.enum(o),c=n.string().regex(/^\w+$/),l=n.record(n.string(),n.unknown()),u=n.number().int().min(100).max(599),d=n.string().startsWith(`/`).refine(e=>!e.includes(`?`)&&!e.includes(`#`),`path must not carry a query or fragment; use query`).refine(e=>e.split(`/`).every(e=>!/[{}]/.test(e)||/^\{[^{}]+\}$/.test(e)),`a {name} placeholder must be one whole path segment`),f=l.refine(e=>A(e),`header names in properties and required must be lower case`),p=n.strictObject({playgrounds:n.array(n.enum([`bbs`,`backlog`,`hosting`])).min(1),expectedRequest:n.strictObject({method:n.enum([`GET`,`POST`,`PUT`,`PATCH`,`DELETE`]),path:d,status:n.union([u,n.array(u).min(1)]).optional(),query:l.optional(),headers:f.optional(),body:l.optional()})}),m=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(c).default([]),requiredOutputFilePaths:n.string().min(1).array().default([]),requiredSubmissionFilePaths:n.string().min(1).array().default([]),agentChallenge:p.optional()}),h={availableLanguageIds:s.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()},g=n.strictObject({...h,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:a,name:n.string().min(1),description:n.string()})).optional()}),_=n.string().array().min(1).refine(e=>e.length===new Set(e).size,{error:`options must contain unique values`}),v=n.string().regex(/^\d+$/,{error:`must be a non-negative integer index (quoted numbers such as "0" are accepted)`}).transform(Number),y={id:a,isResubmittable:n.boolean().optional(),isSurvey:n.boolean().optional(),explanation:n.string().optional(),hint:n.string().optional()},b={...y,type:n.literal(`select`),options:_,answerIndex:n.union([n.number().int().nonnegative(),v,n.number().int().nonnegative().array(),v.array()]).optional()},x={...y,type:n.literal(`select_multiple`),options:_,answerIndices:n.union([n.number().int().nonnegative().array(),v.array()]).optional()},S={...y,type:n.literal(`text`),answerPattern:n.string().min(1).optional(),modelAnswer:n.string().optional()},C=n.discriminatedUnion(`type`,[n.strictObject({...b,question:n.string()}),n.strictObject({...x,question:n.string()}),n.strictObject({...S,question:n.string()})]),w=n.discriminatedUnion(`type`,[n.strictObject({...b,question:n.string().optional()}),n.strictObject({...x,question:n.string().optional()}),n.strictObject({...S,question:n.string().optional()})]),T=n.strictObject({...h,name:n.string().min(1),problems:n.strictObject({id:a}).array().optional(),questions:n.array(w).optional(),turtleGraphicsQuestions:n.object({id:a}).array().optional(),isExamination:n.boolean().optional(),isMockExamination:n.boolean().optional(),isRealtimeSurvey:n.boolean().optional()}),E=`.contest.yaml`,D=n.strictObject({id:a,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`}),O=n.strictObject({id:a,score:n.number().int().nonnegative(),areTestCasesHidden:h.areTestCasesHidden,isDebugHintDisabled:h.isDebugHintDisabled,isFixHintDisabled:h.isFixHintDisabled,isDiffHintDisabled:h.isDiffHintDisabled,debugHintWaitingSeconds:h.debugHintWaitingSeconds,fixHintWaitingSeconds:h.fixHintWaitingSeconds,diffHintWaitingSeconds:h.diffHintWaitingSeconds,isMaterialChatDisabled:h.isMaterialChatDisabled}),k=n.strictObject({...h,name:n.string().min(1),description:n.string().optional(),adminEmails:n.array(n.string().trim().email()).optional(),showsProblemsAfterClose:n.boolean().optional(),divisions:n.array(D).min(1),problems:n.array(O).min(1)});function A(e,t=/* @__PURE__ */ new WeakSet){if(typeof e!=`object`||!e||t.has(e))return!0;if(t.add(e),Array.isArray(e))return e.every(e=>A(e,t));if(!r(e))return!0;let n=r(e.properties)?Object.keys(e.properties):[],i=Array.isArray(e.required)?e.required:[];return[...n,...i].every(e=>typeof e!=`string`||e===e.toLowerCase())&&Object.values(e).every(e=>A(e,t))}export{E as CONTEST_MATERIAL_FILE_SUFFIX,i as LEARNING_MATERIAL_ID_REGEX,p as agentChallengeSchema,o as availableLanguageIds,k as contestFileSchema,g as courseFileSchema,a as learningMaterialIdSchema,T as materialFrontmatterSchema,m as problemFrontmatterSchema,C as questionInCodeBlockSchema};
2
2
  //# sourceMappingURL=schemas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","names":["LEARNING_MATERIAL_ID_REGEX","learningMaterialIdSchema","z","availableLanguageIds","languageIdToDefinition","availableLanguageIdSchema","environmentVariableNameSchema","problemFrontmatterSchema","codeRuleSchema","materialConfigShape","courseFileSchema","uniqueOptionsSchema","options","answerIndexStringSchema","questionBaseShape","selectQuestionShape","selectMultipleQuestionShape","textQuestionShape","questionInCodeBlockSchema","questionInFrontmatterSchema","materialFrontmatterSchema","CONTEST_MATERIAL_FILE_SUFFIX","contestDivisionSchema","division","contestProblemSchema","contestFileSchema"],"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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"}
1
+ {"version":3,"file":"schemas.js","names":["LEARNING_MATERIAL_ID_REGEX","learningMaterialIdSchema","z","availableLanguageIds","languageIdToDefinition","availableLanguageIdSchema","environmentVariableNameSchema","jsonSchemaSchema","httpStatusSchema","pathTemplateSchema","path","segment","headersJsonSchemaSchema","declaresOnlyLowerCaseNames","schema","agentChallengeSchema","problemFrontmatterSchema","codeRuleSchema","materialConfigShape","courseFileSchema","uniqueOptionsSchema","options","answerIndexStringSchema","questionBaseShape","selectQuestionShape","selectMultipleQuestionShape","textQuestionShape","questionInCodeBlockSchema","questionInFrontmatterSchema","materialFrontmatterSchema","CONTEST_MATERIAL_FILE_SUFFIX","contestDivisionSchema","division","contestProblemSchema","contestFileSchema","visited","item","isRecord","propertyNames","requiredNames","name","value"],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { isRecord } from '@willbooster/shared-lib';\nimport { 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\nconst jsonSchemaSchema = z.record(z.string(), z.unknown());\nconst httpStatusSchema = z.number().int().min(100).max(599);\nconst pathTemplateSchema = z\n .string()\n .startsWith('/')\n .refine((path) => !path.includes('?') && !path.includes('#'), 'path must not carry a query or fragment; use query')\n .refine(\n (path) => path.split('/').every((segment) => !/[{}]/.test(segment) || /^\\{[^{}]+\\}$/.test(segment)),\n 'a {name} placeholder must be one whole path segment'\n );\n// Header names are lower-cased before matching, so a capitalized name could never match.\nconst headersJsonSchemaSchema = jsonSchemaSchema.refine(\n (schema) => declaresOnlyLowerCaseNames(schema),\n 'header names in properties and required must be lower case'\n);\n\n// Mirrors the judge server's `agentChallengeSchema`.\nexport const agentChallengeSchema = z.strictObject({\n playgrounds: z.array(z.enum(['bbs', 'backlog', 'hosting'])).min(1),\n expectedRequest: z.strictObject({\n method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),\n path: pathTemplateSchema,\n status: z.union([httpStatusSchema, z.array(httpStatusSchema).min(1)]).optional(),\n query: jsonSchemaSchema.optional(),\n headers: headersJsonSchemaSchema.optional(),\n body: jsonSchemaSchema.optional(),\n }),\n});\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 agentChallenge: agentChallengeSchema.optional(),\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.strictObject({ id: learningMaterialIdSchema }).array().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 // 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\n/** Checks every `properties` key and `required` item at any depth, so composed schemas (allOf, $defs, if/then) count too. */\nfunction declaresOnlyLowerCaseNames(schema: unknown, visited = new WeakSet<object>()): boolean {\n // A YAML alias can make the schema cyclic.\n if (typeof schema !== 'object' || schema === null || visited.has(schema)) return true;\n visited.add(schema);\n if (Array.isArray(schema)) return schema.every((item) => declaresOnlyLowerCaseNames(item, visited));\n if (!isRecord(schema)) return true;\n const propertyNames = isRecord(schema.properties) ? Object.keys(schema.properties) : [];\n const requiredNames: unknown[] = Array.isArray(schema.required) ? schema.required : [];\n return (\n [...propertyNames, ...requiredNames].every((name) => typeof name !== 'string' || name === name.toLowerCase()) &&\n Object.values(schema).every((value) => declaresOnlyLowerCaseNames(value, visited))\n );\n}\n"],"mappings":"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"}
@@ -1,2 +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("@willbooster/shared-lib"),s=require("yaml");async function c(c,l={}){let u=[],d=[],f={errors:u,warnings:d};if(!await t.isRegularFile(c))return u.push(await t.isFile(c)?`contest file is a symbolic link, which the judge ignores; commit a regular file: ${c}`:`contest file not found: ${c}`),f;let p=(0,i.basename)(c);if(!p.endsWith(`.contest.yaml`))return u.push(`contest file name must end with ${e.CONTEST_MATERIAL_FILE_SUFFIX}: ${p}`),f;let m=p.slice(0,-e.CONTEST_MATERIAL_FILE_SUFFIX.length);e.LEARNING_MATERIAL_ID_REGEX.test(m)||u.push(`contest ID "${m}" (file name without ${e.CONTEST_MATERIAL_FILE_SUFFIX}) must match ${e.LEARNING_MATERIAL_ID_REGEX}`);let h;try{h=(0,s.parse)(await(0,a.readFile)(c,`utf8`))}catch(e){return u.push(`invalid YAML: ${(0,o.getErrorMessage)(e)}`),f}let g=e.contestFileSchema.safeParse(h);if(!g.success)return u.push(...n.formatZodIssues(g.error,`contest`)),f;r.validateSubmissionPeriods(r.mergeSubmissionPeriods(g.data,l.courseSubmissionPeriods),u,g.data),r.reportForeignProblemCourseIds(g.data.problems,l.courseId,u),n.reportDuplicateIds(g.data.divisions.map(e=>e.id),`division`,u),n.reportDuplicateIds(g.data.problems.map(e=>e.id),`problem`,u);let _=l.availableProblemIds;return _===void 0&&(l.problemsDirectoryPath===void 0?l.isNestedInCourse||d.push(`no problems directory given (pass --problems-dir); problem references are not checked`):_=await t.collectProblemIdsFromDirectory(l.problemsDirectoryPath,u)),_!==void 0&&t.reportDanglingProblemReferences(g.data.problems.map(e=>e.id),_,l.problemsDirectoryPath,u),f}exports.validateContestFile=c;
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("@willbooster/shared-lib"),s=require("yaml");async function c(c,l={}){let u=[],d=[],f={errors:u,warnings:d};if(!await t.isRegularFile(c))return u.push(await t.isFile(c)?`contest file is a symbolic link, which the judge ignores; commit a regular file: ${c}`:`contest file not found: ${c}`),f;let p=(0,i.basename)(c);if(!p.endsWith(`.contest.yaml`))return u.push(`contest file name must end with ${e.CONTEST_MATERIAL_FILE_SUFFIX}: ${p}`),f;let m=p.slice(0,-e.CONTEST_MATERIAL_FILE_SUFFIX.length);e.LEARNING_MATERIAL_ID_REGEX.test(m)||u.push(`contest ID "${m}" (file name without ${e.CONTEST_MATERIAL_FILE_SUFFIX}) must match ${e.LEARNING_MATERIAL_ID_REGEX}`);let h;try{h=(0,s.parse)(await(0,a.readFile)(c,`utf8`))}catch(e){return u.push(`invalid YAML: ${(0,o.getErrorMessage)(e)}`),f}let g=e.contestFileSchema.safeParse(h);if(!g.success)return u.push(...n.formatZodIssues(g.error,`contest`)),f;r.validateSubmissionPeriods(r.mergeSubmissionPeriods(g.data,l.courseSubmissionPeriods),u,g.data),n.reportDuplicateIds(g.data.divisions.map(e=>e.id),`division`,u),n.reportDuplicateIds(g.data.problems.map(e=>e.id),`problem`,u);let _=l.availableProblemIds;return _===void 0&&(l.problemsDirectoryPath===void 0?l.isNestedInCourse||d.push(`no problems directory given (pass --problems-dir); problem references are not checked`):_=await t.collectProblemIdsFromDirectory(l.problemsDirectoryPath,u)),_!==void 0&&t.reportDanglingProblemReferences(g.data.problems.map(e=>e.id),_,l.problemsDirectoryPath,u),f}exports.validateContestFile=c;
2
2
  //# sourceMappingURL=validateContest.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateContest.cjs","names":["isRegularFile","isFile","basename","CONTEST_MATERIAL_FILE_SUFFIX","LEARNING_MATERIAL_ID_REGEX","parseYaml","readFile","getErrorMessage","contestFileSchema","formatZodIssues","mergeSubmissionPeriods","collectProblemIdsFromDirectory","require_schemas","require_fsHelpers","require_validationResult","require_validateMaterial","node_path","node_fs_promises","_willbooster_shared_lib","yaml","validateContestFile","errors","warnings","result","contestFilePath","fileName","contestId","rawContent","error","parsed","options","division","problem","availableProblemIds"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\nimport { 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 type SubmissionPeriods,\n validateSubmissionPeriods,\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: ${getErrorMessage(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":"mQA8BA,eAAsBoB,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAMC,EAAmB,CAAC,EACpBC,EAAqB,CAAC,EACtBC,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAMvB,EAAAA,cAAcwB,CAAe,EAMvC,OALAH,EAAO,KACJ,MAAMpB,EAAAA,OAAOuB,CAAe,EACzB,oFAAoFA,IACpF,2BAA2BA,GACjC,EACOD,EAET,IAAME,GAAA,EAAWvB,EAAAA,SAAAA,CAASsB,CAAe,EACzC,GAAI,CAACC,EAAS,SAAA,eAAqC,EAEjD,OADAJ,EAAO,KAAK,mCAAmClB,EAAAA,6BAA6B,IAAIsB,GAAU,EACnFF,EAET,IAAMG,EAAYD,EAAS,MAAM,EAAG,CAACtB,EAAAA,6BAA6B,MAAM,EACnEC,EAAAA,2BAA2B,KAAKsB,CAAS,GAC5CL,EAAO,KACL,eAAeK,EAAU,uBAAuBvB,EAAAA,6BAA6B,eAAeC,EAAAA,4BAC9F,EAGF,IAAIuB,EACJ,GAAI,CACF,GAAA,EAAatB,EAAAA,MAAAA,CAAU,MAAA,EAAMC,EAAAA,SAAAA,CAASkB,EAAiB,MAAM,CAAC,CAChE,OAASI,EAAO,CAEd,OADAP,EAAO,KAAK,kBAAA,EAAiBd,EAAAA,gBAAAA,CAAgBqB,CAAK,GAAG,EAC9CL,CACT,CACA,IAAMM,EAASrB,EAAAA,kBAAkB,UAAUmB,CAAU,EACrD,GAAI,CAACE,EAAO,QAEV,OADAR,EAAO,KAAK,GAAGZ,EAAAA,gBAAgBoB,EAAO,MAAO,SAAS,CAAC,EAChDN,EAGTR,EAAA,0BAA0BL,EAAAA,uBAAuBmB,EAAO,KAAMC,EAAQ,uBAAuB,EAAGT,EAAQQ,EAAO,IAAI,EACnHd,EAAA,8BAA8Bc,EAAO,KAAK,SAAUC,EAAQ,SAAUT,CAAM,EAC5EP,EAAA,mBACEe,EAAO,KAAK,UAAU,IAAK,GAAaE,EAAS,EAAE,EACnD,WACAV,CACF,EACAP,EAAA,mBACEe,EAAO,KAAK,SAAS,IAAK,GAAYG,EAAQ,EAAE,EAChD,UACAX,CACF,EAEA,IAAIY,EAAsBH,EAAQ,oBAkBlC,OAjBIG,IAAwB,IAAA,KACtBH,EAAQ,wBAA0B,IAAA,GAC/BA,EAAQ,kBACXR,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAMX,EAAAA,+BAA+BmB,EAAQ,sBAAuBT,CAAM,GAGhGY,IAAwB,IAAA,IAC1BpB,EAAA,gCACEgB,EAAO,KAAK,SAAS,IAAK,GAAYG,EAAQ,EAAE,EAChDC,EACAH,EAAQ,sBACRT,CACF,EAEKE,CACT"}
1
+ {"version":3,"file":"validateContest.cjs","names":["isRegularFile","isFile","basename","CONTEST_MATERIAL_FILE_SUFFIX","LEARNING_MATERIAL_ID_REGEX","parseYaml","readFile","getErrorMessage","contestFileSchema","formatZodIssues","mergeSubmissionPeriods","collectProblemIdsFromDirectory","require_schemas","require_fsHelpers","require_validationResult","require_validateMaterial","node_path","node_fs_promises","_willbooster_shared_lib","yaml","validateContestFile","errors","warnings","result","contestFilePath","fileName","contestId","rawContent","error","parsed","options","division","problem","availableProblemIds"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport { mergeSubmissionPeriods, type SubmissionPeriods, validateSubmissionPeriods } 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 /** 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: ${getErrorMessage(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 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":"A,mQAuBA,eAAsBoB,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAMC,EAAmB,CAAC,EACpBC,EAAqB,CAAC,EACtBC,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAMvB,EAAAA,cAAcwB,CAAe,EAMvC,OALAH,EAAO,KACJ,MAAMpB,EAAAA,OAAOuB,CAAe,EACzB,oFAAoFA,IACpF,2BAA2BA,GACjC,EACOD,EAET,IAAME,GAAA,EAAWvB,EAAAA,SAAAA,CAASsB,CAAe,EACzC,GAAI,CAACC,EAAS,SAAA,eAAqC,EAEjD,OADAJ,EAAO,KAAK,mCAAmClB,EAAAA,6BAA6B,IAAIsB,GAAU,EACnFF,EAET,IAAMG,EAAYD,EAAS,MAAM,EAAG,CAACtB,EAAAA,6BAA6B,MAAM,EACnEC,EAAAA,2BAA2B,KAAKsB,CAAS,GAC5CL,EAAO,KACL,eAAeK,EAAU,uBAAuBvB,EAAAA,6BAA6B,eAAeC,EAAAA,4BAC9F,EAGF,IAAIuB,EACJ,GAAI,CACF,GAAA,EAAatB,EAAAA,MAAAA,CAAU,MAAA,EAAMC,EAAAA,SAAAA,CAASkB,EAAiB,MAAM,CAAC,CAChE,OAASI,EAAO,CAEd,OADAP,EAAO,KAAK,kBAAA,EAAiBd,EAAAA,gBAAAA,CAAgBqB,CAAK,GAAG,EAC9CL,CACT,CACA,IAAMM,EAASrB,EAAAA,kBAAkB,UAAUmB,CAAU,EACrD,GAAI,CAACE,EAAO,QAEV,OADAR,EAAO,KAAK,GAAGZ,EAAAA,gBAAgBoB,EAAO,MAAO,SAAS,CAAC,EAChDN,EAGTR,EAAA,0BAA0BL,EAAAA,uBAAuBmB,EAAO,KAAMC,EAAQ,uBAAuB,EAAGT,EAAQQ,EAAO,IAAI,EACnHf,EAAA,mBACEe,EAAO,KAAK,UAAU,IAAK,GAAaE,EAAS,EAAE,EACnD,WACAV,CACF,EACAP,EAAA,mBACEe,EAAO,KAAK,SAAS,IAAK,GAAYG,EAAQ,EAAE,EAChD,UACAX,CACF,EAEA,IAAIY,EAAsBH,EAAQ,oBAkBlC,OAjBIG,IAAwB,IAAA,KACtBH,EAAQ,wBAA0B,IAAA,GAC/BA,EAAQ,kBACXR,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAMX,EAAAA,+BAA+BmB,EAAQ,sBAAuBT,CAAM,GAGhGY,IAAwB,IAAA,IAC1BpB,EAAA,gCACEgB,EAAO,KAAK,SAAS,IAAK,GAAYG,EAAQ,EAAE,EAChDC,EACAH,EAAQ,sBACRT,CACF,EAEKE,CACT,C"}
@@ -5,8 +5,6 @@ export interface ContestValidationOptions {
5
5
  problemsDirectoryPath?: string;
6
6
  /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */
7
7
  availableProblemIds?: ReadonlySet<string>;
8
- /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */
9
- courseId?: string;
10
8
  /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */
11
9
  courseSubmissionPeriods?: SubmissionPeriods;
12
10
  /** Set by course validation, which already warns once about a missing problems directory. */
@@ -1,2 +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{getErrorMessage as m}from"@willbooster/shared-lib";import{parse as h}from"yaml";async function g(g,_={}){let v=[],y=[],b={errors:v,warnings:y};if(!await a(g))return v.push(await i(g)?`contest file is a symbolic link, which the judge ignores; commit a regular file: ${g}`:`contest file not found: ${g}`),b;let x=f(g);if(!x.endsWith(`.contest.yaml`))return v.push(`contest file name must end with ${e}: ${x}`),b;let S=x.slice(0,-e.length);t.test(S)||v.push(`contest ID "${S}" (file name without ${e}) must match ${t}`);let C;try{C=h(await p(g,`utf8`))}catch(e){return v.push(`invalid YAML: ${m(e)}`),b}let w=n.safeParse(C);if(!w.success)return v.push(...s(w.error,`contest`)),b;d(l(w.data,_.courseSubmissionPeriods),v,w.data),u(w.data.problems,_.courseId,v),c(w.data.divisions.map(e=>e.id),`division`,v),c(w.data.problems.map(e=>e.id),`problem`,v);let T=_.availableProblemIds;return T===void 0&&(_.problemsDirectoryPath===void 0?_.isNestedInCourse||y.push(`no problems directory given (pass --problems-dir); problem references are not checked`):T=await r(_.problemsDirectoryPath,v)),T!==void 0&&o(w.data.problems.map(e=>e.id),T,_.problemsDirectoryPath,v),b}export{g as validateContestFile};
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,validateSubmissionPeriods as u}from"./validateMaterial.js";import{basename as d}from"node:path";import{readFile as f}from"node:fs/promises";import{getErrorMessage as p}from"@willbooster/shared-lib";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=d(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 f(h,`utf8`))}catch(e){return _.push(`invalid YAML: ${p(e)}`),y}let C=n.safeParse(S);if(!C.success)return _.push(...s(C.error,`contest`)),y;u(l(C.data,g.courseSubmissionPeriods),_,C.data),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
2
  //# sourceMappingURL=validateContest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateContest.js","names":["parseYaml","validateContestFile","errors","warnings","result","isRegularFile","contestFilePath","isFile","fileName","basename","CONTEST_MATERIAL_FILE_SUFFIX","contestId","LEARNING_MATERIAL_ID_REGEX","rawContent","parse","readFile","error","getErrorMessage","parsed","contestFileSchema","formatZodIssues","validateSubmissionPeriods","mergeSubmissionPeriods","options","reportForeignProblemCourseIds","reportDuplicateIds","division","problem","availableProblemIds","collectProblemIdsFromDirectory","reportDanglingProblemReferences"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\nimport { 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 type SubmissionPeriods,\n validateSubmissionPeriods,\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: ${getErrorMessage(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":"gnBA8BA,eAAsBC,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAMC,EAAmB,CAAC,EACpBC,EAAqB,CAAC,EACtBC,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAMC,EAAcC,CAAe,EAMvC,OALAJ,EAAO,KACJ,MAAMK,EAAOD,CAAe,EACzB,oFAAoFA,IACpF,2BAA2BA,GACjC,EACOF,EAET,IAAMI,EAAWC,EAASH,CAAe,EACzC,GAAI,CAACE,EAAS,SAAA,eAAqC,EAEjD,OADAN,EAAO,KAAK,mCAAmCQ,EAA6B,IAAIF,GAAU,EACnFJ,EAET,IAAMO,EAAYH,EAAS,MAAM,EAAG,CAACE,EAA6B,MAAM,EACnEE,EAA2B,KAAKD,CAAS,GAC5CT,EAAO,KACL,eAAeS,EAAU,uBAAuBD,EAA6B,eAAeE,GAC9F,EAGF,IAAIC,EACJ,GAAI,CACF,EAAab,EAAU,MAAMe,EAAST,EAAiB,MAAM,CAAC,CAChE,OAASU,EAAO,CAEd,OADAd,EAAO,KAAK,iBAAiBe,EAAgBD,CAAK,GAAG,EAC9CZ,CACT,CACA,IAAMc,EAASC,EAAkB,UAAUN,CAAU,EACrD,GAAI,CAACK,EAAO,QAEV,OADAhB,EAAO,KAAK,GAAGkB,EAAgBF,EAAO,MAAO,SAAS,CAAC,EAChDd,EAGTiB,EAA0BC,EAAuBJ,EAAO,KAAMK,EAAQ,uBAAuB,EAAGrB,EAAQgB,EAAO,IAAI,EACnHM,EAA8BN,EAAO,KAAK,SAAUK,EAAQ,SAAUrB,CAAM,EAC5EuB,EACEP,EAAO,KAAK,UAAU,IAAK,GAAaQ,EAAS,EAAE,EACnD,WACAxB,CACF,EACAuB,EACEP,EAAO,KAAK,SAAS,IAAK,GAAYS,EAAQ,EAAE,EAChD,UACAzB,CACF,EAEA,IAAI0B,EAAsBL,EAAQ,oBAkBlC,OAjBIK,IAAwB,IAAA,KACtBL,EAAQ,wBAA0B,IAAA,GAC/BA,EAAQ,kBACXpB,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAM0B,EAA+BN,EAAQ,sBAAuBrB,CAAM,GAGhG0B,IAAwB,IAAA,IAC1BE,EACEZ,EAAO,KAAK,SAAS,IAAK,GAAYS,EAAQ,EAAE,EAChDC,EACAL,EAAQ,sBACRrB,CACF,EAEKE,CACT"}
1
+ {"version":3,"file":"validateContest.js","names":["parseYaml","validateContestFile","errors","warnings","result","isRegularFile","contestFilePath","isFile","fileName","basename","CONTEST_MATERIAL_FILE_SUFFIX","contestId","LEARNING_MATERIAL_ID_REGEX","rawContent","parse","readFile","error","getErrorMessage","parsed","contestFileSchema","formatZodIssues","validateSubmissionPeriods","mergeSubmissionPeriods","options","reportDuplicateIds","division","problem","availableProblemIds","collectProblemIdsFromDirectory","reportDanglingProblemReferences"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport { mergeSubmissionPeriods, type SubmissionPeriods, validateSubmissionPeriods } 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 /** 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: ${getErrorMessage(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 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":"A,6kBAuBA,eAAsBC,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAMC,EAAmB,CAAC,EACpBC,EAAqB,CAAC,EACtBC,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAMC,EAAcC,CAAe,EAMvC,OALAJ,EAAO,KACJ,MAAMK,EAAOD,CAAe,EACzB,oFAAoFA,IACpF,2BAA2BA,GACjC,EACOF,EAET,IAAMI,EAAWC,EAASH,CAAe,EACzC,GAAI,CAACE,EAAS,SAAA,eAAqC,EAEjD,OADAN,EAAO,KAAK,mCAAmCQ,EAA6B,IAAIF,GAAU,EACnFJ,EAET,IAAMO,EAAYH,EAAS,MAAM,EAAG,CAACE,EAA6B,MAAM,EACnEE,EAA2B,KAAKD,CAAS,GAC5CT,EAAO,KACL,eAAeS,EAAU,uBAAuBD,EAA6B,eAAeE,GAC9F,EAGF,IAAIC,EACJ,GAAI,CACF,EAAab,EAAU,MAAMe,EAAST,EAAiB,MAAM,CAAC,CAChE,OAASU,EAAO,CAEd,OADAd,EAAO,KAAK,iBAAiBe,EAAgBD,CAAK,GAAG,EAC9CZ,CACT,CACA,IAAMc,EAASC,EAAkB,UAAUN,CAAU,EACrD,GAAI,CAACK,EAAO,QAEV,OADAhB,EAAO,KAAK,GAAGkB,EAAgBF,EAAO,MAAO,SAAS,CAAC,EAChDd,EAGTiB,EAA0BC,EAAuBJ,EAAO,KAAMK,EAAQ,uBAAuB,EAAGrB,EAAQgB,EAAO,IAAI,EACnHM,EACEN,EAAO,KAAK,UAAU,IAAK,GAAaO,EAAS,EAAE,EACnD,WACAvB,CACF,EACAsB,EACEN,EAAO,KAAK,SAAS,IAAK,GAAYQ,EAAQ,EAAE,EAChD,UACAxB,CACF,EAEA,IAAIyB,EAAsBJ,EAAQ,oBAkBlC,OAjBII,IAAwB,IAAA,KACtBJ,EAAQ,wBAA0B,IAAA,GAC/BA,EAAQ,kBACXpB,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAMyB,EAA+BL,EAAQ,sBAAuBrB,CAAM,GAGhGyB,IAAwB,IAAA,IAC1BE,EACEX,EAAO,KAAK,SAAS,IAAK,GAAYQ,EAAQ,EAAE,EAChDC,EACAJ,EAAQ,sBACRrB,CACF,EAEKE,CACT,C"}
@@ -1,2 +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("@willbooster/shared-lib"),c=require("yaml");async function l(i,s={}){let c=[],l=[],p={errors:c,warnings:l};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 m=await(0,o.realpath)(i),g=s.problemsDirectoryPath===void 0?void 0:await u(s.problemsDirectoryPath),_=(0,a.basename)(m);e.LEARNING_MATERIAL_ID_REGEX.test(_)||c.push(`course ID "${_}" (directory name) must match ${e.LEARNING_MATERIAL_ID_REGEX}`);let y=await f(m,c),b=m,x=await h(m,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&&l.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 v(m,{courseId:_,courseSubmissionPeriods:S,problemsDirectoryPath:b,availableProblemIds:x},e.id,c,l);return await d(m,S,C,l),p}async function u(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 d(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()&&m(e.name))&&r.push(`directory "${t.name}" is not listed as a lecture in ${n}, so its materials are not imported`)}async function f(r,i){let l=[`course.yaml`,`course.yml`],u=[];for(let e of l)await t.isRegularFile((0,a.join)(r,e))&&u.push(e);if(u.length>1){i.push(`both course.yaml and course.yml exist; the judge rejects the conflicting course files`);return}let d=u[0];if(d===void 0){let e=await p(l,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 f=(0,a.join)(r,d),m;try{m=(0,c.parse)(await(0,o.readFile)(f,`utf8`))}catch(e){i.push(`${d}: invalid YAML: ${(0,s.getErrorMessage)(e)}`);return}let h=e.courseFileSchema.safeParse(m);if(!h.success)i.push(...n.formatZodIssues(h.error,d));else return{courseFile:h.data,courseFileName:d}}async function p(e,t){for(let n of e)if(await t(n))return n}function m(e){return e.endsWith(`.contest.yaml`)||e.endsWith(`.md`)&&e!==`problem.md`&&!e.endsWith(`.problem.md`)}async function h(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 g(r,c);return(l||await t.isDirectory(c))&&await t.isUsableProblemsDirectory(c,i),r===void 0||l||await t.isUsableProblemsDirectory(r,i)&&_(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 g(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 _(e,t){let n=(0,a.relative)(e,t);return n===`..`||n.startsWith(`..${a.sep}`)}async function v(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=l;
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("@willbooster/shared-lib"),c=require("yaml");async function l(i,s={}){let c=[],l=[],p={errors:c,warnings:l};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 m=await(0,o.realpath)(i),g=s.problemsDirectoryPath===void 0?void 0:await u(s.problemsDirectoryPath),_=(0,a.basename)(m);e.LEARNING_MATERIAL_ID_REGEX.test(_)||c.push(`course ID "${_}" (directory name) must match ${e.LEARNING_MATERIAL_ID_REGEX}`);let y=await f(m,c),b=m,x=await h(m,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&&l.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 v(m,{courseSubmissionPeriods:S,problemsDirectoryPath:b,availableProblemIds:x},e.id,c,l);return await d(m,S,C,l),p}async function u(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 d(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()&&m(e.name))&&r.push(`directory "${t.name}" is not listed as a lecture in ${n}, so its materials are not imported`)}async function f(r,i){let l=[`course.yaml`,`course.yml`],u=[];for(let e of l)await t.isRegularFile((0,a.join)(r,e))&&u.push(e);if(u.length>1){i.push(`both course.yaml and course.yml exist; the judge rejects the conflicting course files`);return}let d=u[0];if(d===void 0){let e=await p(l,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 f=(0,a.join)(r,d),m;try{m=(0,c.parse)(await(0,o.readFile)(f,`utf8`))}catch(e){i.push(`${d}: invalid YAML: ${(0,s.getErrorMessage)(e)}`);return}let h=e.courseFileSchema.safeParse(m);if(!h.success)i.push(...n.formatZodIssues(h.error,d));else return{courseFile:h.data,courseFileName:d}}async function p(e,t){for(let n of e)if(await t(n))return n}function m(e){return e.endsWith(`.contest.yaml`)||e.endsWith(`.md`)&&e!==`problem.md`&&!e.endsWith(`.problem.md`)}async function h(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 g(r,c);return(l||await t.isDirectory(c))&&await t.isUsableProblemsDirectory(c,i),r===void 0||l||await t.isUsableProblemsDirectory(r,i)&&_(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 g(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 _(e,t){let n=(0,a.relative)(e,t);return n===`..`||n.startsWith(`..${a.sep}`)}async function v(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=l;
2
2
  //# sourceMappingURL=validateCourse.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateCourse.cjs","names":["isRegularDirectory","resolve","isDirectory","realpath","basename","LEARNING_MATERIAL_ID_REGEX","join","dirname","readdir","isRegularFile","isFile","parseYaml","readFile","getErrorMessage","courseFileSchema","formatZodIssues","collectProblemDefinitions","isUsableProblemsDirectory","lstat","relative","sep","CONTEST_MATERIAL_FILE_SUFFIX","validateContestFile","validateMaterialFile","require_schemas","require_fsHelpers","require_validationResult","require_validateMaterial","require_validateContest","node_path","node_fs_promises","_willbooster_shared_lib","yaml","validateCourseDirectory","errors","warnings","result","courseDirectoryPath","absoluteDirectoryPath","explicitProblemsDirectoryPath","options","canonicalize","courseId","parsedCourseFile","parseCourseFile","problemsDirectoryPath","availableProblemIds","collectAvailableProblemIds","coursePeriodErrors","courseFile","courseFileName","error","lectures","lecture","validateLectureDirectory","reportOrphanLectureDirectories","absolutePath","path","lectureIds","dirents","dirent","entry","isMaterialFileName","courseFileNames","presentFileNames","fileName","symlinkFileName","findFirst","courseFilePath","rawContent","parsed","item","items","predicate","definitionPathsById","courseProblemsDirectoryPath","namesConventionalDirectory","isSameEntry","isOutsideDirectory","problemsDirectoryOption","path1","path2","stats1","stats2","relativePath","parentPath","lectureDirectoryPath","lectureId","materialIds","fileNames","f1","f2","materialFilePath","materialResult","materialOptions","warning","duplicateMaterialErrors"],"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';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\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 type CourseFile,\n courseFileSchema,\n LEARNING_MATERIAL_ID_REGEX,\n} from './schemas.js';\nimport { validateContestFile } from './validateContest.js';\nimport { type MaterialValidationOptions, validateMaterialFile, validateSubmissionPeriods } 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: ${getErrorMessage(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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"}
1
+ {"version":3,"file":"validateCourse.cjs","names":["isRegularDirectory","resolve","isDirectory","realpath","basename","LEARNING_MATERIAL_ID_REGEX","join","dirname","readdir","isRegularFile","isFile","parseYaml","readFile","getErrorMessage","courseFileSchema","formatZodIssues","collectProblemDefinitions","isUsableProblemsDirectory","lstat","relative","sep","CONTEST_MATERIAL_FILE_SUFFIX","validateContestFile","validateMaterialFile","require_schemas","require_fsHelpers","require_validationResult","require_validateMaterial","require_validateContest","node_path","node_fs_promises","_willbooster_shared_lib","yaml","validateCourseDirectory","errors","warnings","result","courseDirectoryPath","absoluteDirectoryPath","explicitProblemsDirectoryPath","options","canonicalize","courseId","parsedCourseFile","parseCourseFile","problemsDirectoryPath","availableProblemIds","collectAvailableProblemIds","coursePeriodErrors","courseFile","courseFileName","error","lectures","lecture","validateLectureDirectory","reportOrphanLectureDirectories","absolutePath","path","lectureIds","dirents","dirent","entry","isMaterialFileName","courseFileNames","presentFileNames","fileName","symlinkFileName","findFirst","courseFilePath","rawContent","parsed","item","items","predicate","definitionPathsById","courseProblemsDirectoryPath","namesConventionalDirectory","isSameEntry","isOutsideDirectory","problemsDirectoryOption","path1","path2","stats1","stats2","relativePath","parentPath","lectureDirectoryPath","lectureId","materialIds","fileNames","f1","f2","materialFilePath","materialResult","materialOptions","warning","duplicateMaterialErrors"],"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';\n\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\n\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 type CourseFile,\n courseFileSchema,\n LEARNING_MATERIAL_ID_REGEX,\n} from './schemas.js';\nimport { validateContestFile } from './validateContest.js';\nimport { type MaterialValidationOptions, validateMaterialFile, validateSubmissionPeriods } 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 { 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: ${getErrorMessage(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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"}
@@ -1,2 +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{getErrorMessage as T}from"@willbooster/shared-lib";import{parse as E}from"yaml";async function D(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 O(n.problemsDirectoryPath),u=h(c);t.test(u)||r.push(`course ID "${u}" (directory name) must match ${t}`);let f=await A(c,r),m=c,g=await N(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 I(c,{courseId:u,courseSubmissionPeriods:_,problemsDirectoryPath:m,availableProblemIds:g},e.id,r,a);return await k(c,_,v,a),s}async function O(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 k(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()&&M(e.name))&&r.push(`directory "${t.name}" is not listed as a lecture in ${n}, so its materials are not imported`)}async function A(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 j(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=E(await S(c,`utf8`))}catch(e){t.push(`${o}: invalid YAML: ${T(e)}`);return}let d=n.safeParse(l);if(!d.success)t.push(...u(d.error,o));else return{courseFile:d.data,courseFileName:o}}async function j(e,t){for(let n of e)if(await t(n))return n}function M(e){return e.endsWith(`.contest.yaml`)||e.endsWith(`.md`)&&e!==`problem.md`&&!e.endsWith(`.problem.md`)}async function N(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 P(n,u);return(d||await i(u))&&await c(u,a),n===void 0||d||await c(n,a)&&F(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 P(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 F(e,t){let n=v(e,t);return n===`..`||n.startsWith(`..${b}`)}async function I(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{D as validateCourseDirectory};
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{getErrorMessage as T}from"@willbooster/shared-lib";import{parse as E}from"yaml";async function D(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 O(n.problemsDirectoryPath),u=h(c);t.test(u)||r.push(`course ID "${u}" (directory name) must match ${t}`);let f=await A(c,r),m=c,g=await N(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 I(c,{courseSubmissionPeriods:_,problemsDirectoryPath:m,availableProblemIds:g},e.id,r,a);return await k(c,_,v,a),s}async function O(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 k(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()&&M(e.name))&&r.push(`directory "${t.name}" is not listed as a lecture in ${n}, so its materials are not imported`)}async function A(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 j(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=E(await S(c,`utf8`))}catch(e){t.push(`${o}: invalid YAML: ${T(e)}`);return}let d=n.safeParse(l);if(!d.success)t.push(...u(d.error,o));else return{courseFile:d.data,courseFileName:o}}async function j(e,t){for(let n of e)if(await t(n))return n}function M(e){return e.endsWith(`.contest.yaml`)||e.endsWith(`.md`)&&e!==`problem.md`&&!e.endsWith(`.problem.md`)}async function N(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 P(n,u);return(d||await i(u))&&await c(u,a),n===void 0||d||await c(n,a)&&F(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 P(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 F(e,t){let n=v(e,t);return n===`..`||n.startsWith(`..${b}`)}async function I(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{D as validateCourseDirectory};
2
2
  //# sourceMappingURL=validateCourse.js.map