@exercode/problem-utils 4.3.1 → 4.3.2

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 (107) hide show
  1. package/dist/cli/checkAllProblems.cjs.map +1 -1
  2. package/dist/cli/checkAllProblems.js.map +1 -1
  3. package/dist/cli/exercodeProblem.cjs.map +1 -1
  4. package/dist/cli/exercodeProblem.js.map +1 -1
  5. package/dist/cli/runSingleHarness.cjs.map +1 -1
  6. package/dist/cli/runSingleHarness.js.map +1 -1
  7. package/dist/cli/validateLearningMaterial.cjs.map +1 -1
  8. package/dist/cli/validateLearningMaterial.js.map +1 -1
  9. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  10. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  11. package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
  12. package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
  13. package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
  14. package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
  15. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs.map +1 -1
  16. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js.map +1 -1
  17. package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
  18. package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
  19. package/dist/helpers/defaultStdioHarness.cjs.map +1 -1
  20. package/dist/helpers/defaultStdioHarness.js.map +1 -1
  21. package/dist/helpers/findEntryPointFile.cjs.map +1 -1
  22. package/dist/helpers/findEntryPointFile.js.map +1 -1
  23. package/dist/helpers/findLanguageDefinitionByPath.cjs.map +1 -1
  24. package/dist/helpers/findLanguageDefinitionByPath.js.map +1 -1
  25. package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
  26. package/dist/helpers/findModelAnswerDirs.js.map +1 -1
  27. package/dist/helpers/guiRecording.cjs.map +1 -1
  28. package/dist/helpers/guiRecording.js.map +1 -1
  29. package/dist/helpers/javaJudge.cjs.map +1 -1
  30. package/dist/helpers/javaJudge.js.map +1 -1
  31. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  32. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  33. package/dist/helpers/parseArgs.cjs.map +1 -1
  34. package/dist/helpers/parseArgs.js.map +1 -1
  35. package/dist/helpers/parseCsv.cjs.map +1 -1
  36. package/dist/helpers/parseCsv.js.map +1 -1
  37. package/dist/helpers/printDebugBanner.cjs.map +1 -1
  38. package/dist/helpers/printDebugBanner.js.map +1 -1
  39. package/dist/helpers/printTestCaseResult.cjs.map +1 -1
  40. package/dist/helpers/printTestCaseResult.js.map +1 -1
  41. package/dist/helpers/readOutputFiles.cjs.map +1 -1
  42. package/dist/helpers/readOutputFiles.js.map +1 -1
  43. package/dist/helpers/readProblemMarkdownFrontMatter.cjs.map +1 -1
  44. package/dist/helpers/readProblemMarkdownFrontMatter.js.map +1 -1
  45. package/dist/helpers/readTestCases.cjs.map +1 -1
  46. package/dist/helpers/readTestCases.js.map +1 -1
  47. package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
  48. package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
  49. package/dist/helpers/resolveCwds.cjs.map +1 -1
  50. package/dist/helpers/resolveCwds.js.map +1 -1
  51. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  52. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  53. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  54. package/dist/helpers/runHarnessProcess.js.map +1 -1
  55. package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -1
  56. package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -1
  57. package/dist/helpers/runPythonInTemporaryUvProject.cjs.map +1 -1
  58. package/dist/helpers/runPythonInTemporaryUvProject.js.map +1 -1
  59. package/dist/helpers/sourceCodeGrammars.cjs.map +1 -1
  60. package/dist/helpers/sourceCodeGrammars.js.map +1 -1
  61. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  62. package/dist/helpers/spawnWithLimits.js.map +1 -1
  63. package/dist/helpers/spawnWithTimeout.cjs.map +1 -1
  64. package/dist/helpers/spawnWithTimeout.js.map +1 -1
  65. package/dist/helpers/startHttpServer.cjs.map +1 -1
  66. package/dist/helpers/startHttpServer.js.map +1 -1
  67. package/dist/helpers/stdioJudgeRules.cjs.map +1 -1
  68. package/dist/helpers/stdioJudgeRules.js.map +1 -1
  69. package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -1
  70. package/dist/helpers/temporaryProblemDirCopy.js.map +1 -1
  71. package/dist/learningMaterial/frontmatter.cjs.map +1 -1
  72. package/dist/learningMaterial/frontmatter.js.map +1 -1
  73. package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
  74. package/dist/learningMaterial/fsHelpers.js.map +1 -1
  75. package/dist/learningMaterial/schemas.cjs.map +1 -1
  76. package/dist/learningMaterial/schemas.js.map +1 -1
  77. package/dist/learningMaterial/validateContest.cjs.map +1 -1
  78. package/dist/learningMaterial/validateContest.js.map +1 -1
  79. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  80. package/dist/learningMaterial/validateCourse.js.map +1 -1
  81. package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
  82. package/dist/learningMaterial/validateMaterial.js.map +1 -1
  83. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  84. package/dist/learningMaterial/validateProblem.js.map +1 -1
  85. package/dist/learningMaterial/validationResult.cjs.map +1 -1
  86. package/dist/learningMaterial/validationResult.js.map +1 -1
  87. package/dist/presets/command.cjs.map +1 -1
  88. package/dist/presets/command.js.map +1 -1
  89. package/dist/presets/evaluation.cjs.map +1 -1
  90. package/dist/presets/evaluation.js.map +1 -1
  91. package/dist/presets/guiCommand.cjs.map +1 -1
  92. package/dist/presets/guiCommand.js.map +1 -1
  93. package/dist/presets/springBoot.cjs.map +1 -1
  94. package/dist/presets/springBoot.js.map +1 -1
  95. package/dist/presets/stdio.cjs.map +1 -1
  96. package/dist/presets/stdio.js.map +1 -1
  97. package/dist/presets/tomcat.cjs.map +1 -1
  98. package/dist/presets/tomcat.js.map +1 -1
  99. package/dist/types/decisionCode.cjs.map +1 -1
  100. package/dist/types/decisionCode.js.map +1 -1
  101. package/dist/types/language.cjs.map +1 -1
  102. package/dist/types/language.js.map +1 -1
  103. package/dist/types/problem.cjs.map +1 -1
  104. package/dist/types/problem.js.map +1 -1
  105. package/dist/types/testCaseResult.cjs.map +1 -1
  106. package/dist/types/testCaseResult.js.map +1 -1
  107. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"spawnWithTimeout.cjs","names":["isTimeCommandAvailable","spawnWithLimits"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"sDAWA,eAAsB,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACA,EAAAA,uBAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAM,EAAS,MAAMC,EAAAA,gBAAgB,CAAC,EAAS,GAAG,CAAI,EAAG,CACvD,IAAK,EAAQ,IACb,IAAK,EAAQ,IACb,iBAAkB,QAClB,MAAO,EAAQ,OAAS,GACxB,iBAAkB,CACpB,CAAC,EAIK,EACJ,EAAO,oBAAsB,CAAC,EAAO,SACjC,GAAG,EAAO,SAAS,EAAO,QAAU,CAAC,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAK,EAAO,mBAAmB,IAC1G,EAAO,OAEb,MAAO,CACL,OAAQ,EAAO,OACf,SACA,OAAQ,EAAO,UAAY,EAAO,oBAAsB,EAAI,EAAO,OACnE,YAAa,EAAO,SAAW,EAAiB,KAAO,EAAO,YAC9D,eAAgB,EAAO,eACvB,YAAa,EAAO,YACpB,oBAAqB,EAAO,mBAC9B,CACF"}
1
+ {"version":3,"file":"spawnWithTimeout.cjs","names":["isTimeCommandAvailable","spawnWithLimits","require_spawnWithLimits","spawnWithTimeout","result","command","args","context","timeoutSeconds","stderr"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"sDAWA,eAAsBG,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACH,EAAAA,uBAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAMI,EAAS,MAAMH,EAAAA,gBAAgB,CAACI,EAAS,GAAGC,CAAI,EAAG,CACvD,IAAKC,EAAQ,IACb,IAAKA,EAAQ,IACb,iBAAkB,QAClB,MAAOA,EAAQ,OAAS,GACxB,iBAAkBC,CACpB,CAAC,EAIKC,EACJL,EAAO,oBAAsB,CAACA,EAAO,SACjC,GAAGA,EAAO,SAASA,EAAO,QAAU,CAACA,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAKA,EAAO,mBAAmB,IAC1GA,EAAO,OAEb,MAAO,CACL,OAAQA,EAAO,OACf,SACA,OAAQA,EAAO,UAAYA,EAAO,oBAAsB,EAAIA,EAAO,OACnE,YAAaA,EAAO,SAAWI,EAAiB,KAAOJ,EAAO,YAC9D,eAAgBA,EAAO,eACvB,YAAaA,EAAO,YACpB,oBAAqBA,EAAO,mBAC9B,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"spawnWithTimeout.js","names":[],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"mFAWA,eAAsB,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAAC,EAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAM,EAAS,MAAM,EAAgB,CAAC,EAAS,GAAG,CAAI,EAAG,CACvD,IAAK,EAAQ,IACb,IAAK,EAAQ,IACb,iBAAkB,QAClB,MAAO,EAAQ,OAAS,GACxB,iBAAkB,CACpB,CAAC,EAIK,EACJ,EAAO,oBAAsB,CAAC,EAAO,SACjC,GAAG,EAAO,SAAS,EAAO,QAAU,CAAC,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAK,EAAO,mBAAmB,IAC1G,EAAO,OAEb,MAAO,CACL,OAAQ,EAAO,OACf,SACA,OAAQ,EAAO,UAAY,EAAO,oBAAsB,EAAI,EAAO,OACnE,YAAa,EAAO,SAAW,EAAiB,KAAO,EAAO,YAC9D,eAAgB,EAAO,eACvB,YAAa,EAAO,YACpB,oBAAqB,EAAO,mBAC9B,CACF"}
1
+ {"version":3,"file":"spawnWithTimeout.js","names":["spawnWithTimeout","isTimeCommandAvailable","result","spawnWithLimits","command","args","context","timeoutSeconds","stderr"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"mFAWA,eAAsBA,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACC,EAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAMC,EAAS,MAAMC,EAAgB,CAACC,EAAS,GAAGC,CAAI,EAAG,CACvD,IAAKC,EAAQ,IACb,IAAKA,EAAQ,IACb,iBAAkB,QAClB,MAAOA,EAAQ,OAAS,GACxB,iBAAkBC,CACpB,CAAC,EAIKC,EACJN,EAAO,oBAAsB,CAACA,EAAO,SACjC,GAAGA,EAAO,SAASA,EAAO,QAAU,CAACA,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAKA,EAAO,mBAAmB,IAC1GA,EAAO,OAEb,MAAO,CACL,OAAQA,EAAO,OACf,SACA,OAAQA,EAAO,UAAYA,EAAO,oBAAsB,EAAIA,EAAO,OACnE,YAAaA,EAAO,SAAWK,EAAiB,KAAOL,EAAO,YAC9D,eAAgBA,EAAO,eACvB,YAAaA,EAAO,YACpB,oBAAqBA,EAAO,mBAC9B,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"startHttpServer.cjs","names":["path","http","fs"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"oMAWA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAkB,CAAG,EAEpC,OADA,EAAO,OAAO,EACP,EAAmB,CAAM,CAClC,CAGA,eAAsB,EAAqB,EAAkC,CAC3E,IAAM,EAAS,EAAkB,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,EAAO,KAAK,QAAS,CAAM,EAC3B,EAAO,OAAO,EAAG,gBAAmB,CAClC,EAAO,IAAI,QAAS,CAAM,EAC1B,EAAQ,CACV,CAAC,CACH,CAAC,EACM,EAAmB,CAAM,CAClC,CAEA,SAAS,EAAkB,EAA0B,CACnD,IAAM,EAAWA,EAAAA,QAAK,QAAQ,CAAG,EACjC,OAAOC,EAAAA,QAAK,cAAc,EAAS,IAAa,CAC9C,IAAI,EACJ,GAAI,CACF,IAAM,EAAkB,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmB,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACN,EAAS,UAAU,GAAG,EACtB,EAAS,IAAI,EACb,MACF,CAEI,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAID,EAAAA,QAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAWA,EAAAA,QAAK,QAAQ,EAAU,IAAI,GAAuB,EAC7D,EAAeA,EAAAA,QAAK,SAAS,EAAU,CAAQ,EACrD,GAAI,IAAiB,MAAQ,EAAa,WAAW,KAAKA,EAAAA,QAAK,KAAK,EAAG,CACrE,EAAS,UAAU,GAAG,EACtB,EAAS,IAAI,EACb,MACF,CAEA,GAAIE,EAAAA,QAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAASA,EAAAA,QAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAAS,EAAmB,EAAiC,CAC3D,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
1
+ {"version":3,"file":"startHttpServer.cjs","names":["path","http","fs","require_runtime","node_fs","node_path","node_http","startHttpServer","server","createAssetServer","dir","createServerHandle","startLocalHttpServer","reject","resolve","rootPath","pathname","encodedPathname","request","response","pathnameWithIndexHtml","filePath","relativePath","buffer","getContentType","address","CONTENT_TYPE_BY_SUFFIX","suffix","contentType"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"oMAWA,SAAgBO,EAAgB,EAAyB,CACvD,IAAMC,EAASC,EAAkBC,CAAG,EAEpC,OADAF,EAAO,OAAO,EACPG,EAAmBH,CAAM,CAClC,CAGA,eAAsBI,EAAqB,EAAkC,CAC3E,IAAMJ,EAASC,EAAkBC,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3CF,EAAO,KAAK,QAASK,CAAM,EAC3BL,EAAO,OAAO,EAAG,gBAAmB,CAClCA,EAAO,IAAI,QAASK,CAAM,EAC1BC,EAAQ,CACV,CAAC,CACH,CAAC,EACMH,EAAmBH,CAAM,CAClC,CAEA,SAASC,EAAkB,EAA0B,CACnD,IAAMM,EAAWf,EAAAA,QAAK,QAAQU,CAAG,EACjC,OAAOT,EAAAA,QAAK,cAAc,EAAS,IAAa,CAC9C,IAAIe,EACJ,GAAI,CACF,IAAMC,EAAkB,IAAI,IAAIC,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmBD,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACNE,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEIH,IAAa,MACf,EAAW,eAGb,IAAMI,EAAwBJ,EAAS,SAAS,GAAG,EAAIhB,EAAAA,QAAK,KAAKgB,EAAU,YAAY,EAAIA,EAErFK,EAAWrB,EAAAA,QAAK,QAAQe,EAAU,IAAIK,GAAuB,EAC7DE,EAAetB,EAAAA,QAAK,SAASe,EAAUM,CAAQ,EACrD,GAAIC,IAAiB,MAAQA,EAAa,WAAW,KAAKtB,EAAAA,QAAK,KAAK,EAAG,CACrEmB,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEA,GAAIjB,EAAAA,QAAG,WAAWmB,CAAQ,EACxB,GAAI,CACF,IAAME,EAASrB,EAAAA,QAAG,aAAamB,CAAQ,EACvCF,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgBK,EAAeJ,CAAqB,CACtD,CAAC,EACDD,EAAS,IAAII,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtBJ,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtBA,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAASR,EAAmB,EAAiC,CAC3D,IAAMc,EAAUjB,EAAO,QAAQ,EAC/B,GAAI,CAACiB,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjCjB,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnCA,EAAO,UAAY,CACjBM,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAOW,GAAY,SAAWA,EAAU,oBAAoBA,EAAQ,OACzE,KAAM,OAAOA,GAAY,SAAWA,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAMC,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAASF,EAAe,EAA0B,CAChD,IAAK,GAAM,CAACG,EAAQC,KAAgB,OAAO,QAAQF,CAAsB,EACvE,GAAIV,EAAS,SAASW,CAAM,EAAG,OAAOC,EAExC,MAAO,YACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"startHttpServer.js","names":[],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"yEAWA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAkB,CAAG,EAEpC,OADA,EAAO,OAAO,EACP,EAAmB,CAAM,CAClC,CAGA,eAAsB,EAAqB,EAAkC,CAC3E,IAAM,EAAS,EAAkB,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,EAAO,KAAK,QAAS,CAAM,EAC3B,EAAO,OAAO,EAAG,gBAAmB,CAClC,EAAO,IAAI,QAAS,CAAM,EAC1B,EAAQ,CACV,CAAC,CACH,CAAC,EACM,EAAmB,CAAM,CAClC,CAEA,SAAS,EAAkB,EAA0B,CACnD,IAAM,EAAW,EAAK,QAAQ,CAAG,EACjC,OAAO,EAAK,cAAc,EAAS,IAAa,CAC9C,IAAI,EACJ,GAAI,CACF,IAAM,EAAkB,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmB,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACN,EAAS,UAAU,GAAG,EACtB,EAAS,IAAI,EACb,MACF,CAEI,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAI,EAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAW,EAAK,QAAQ,EAAU,IAAI,GAAuB,EAC7D,EAAe,EAAK,SAAS,EAAU,CAAQ,EACrD,GAAI,IAAiB,MAAQ,EAAa,WAAW,KAAK,EAAK,KAAK,EAAG,CACrE,EAAS,UAAU,GAAG,EACtB,EAAS,IAAI,EACb,MACF,CAEA,GAAI,EAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAAS,EAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAAS,EAAmB,EAAiC,CAC3D,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
1
+ {"version":3,"file":"startHttpServer.js","names":["fs","path","http","startHttpServer","server","createAssetServer","dir","createServerHandle","startLocalHttpServer","reject","resolve","rootPath","pathname","encodedPathname","request","response","pathnameWithIndexHtml","filePath","relativePath","buffer","getContentType","address","CONTENT_TYPE_BY_SUFFIX","suffix","contentType"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"yEAWA,SAAgBG,EAAgB,EAAyB,CACvD,IAAMC,EAASC,EAAkBC,CAAG,EAEpC,OADAF,EAAO,OAAO,EACPG,EAAmBH,CAAM,CAClC,CAGA,eAAsBI,EAAqB,EAAkC,CAC3E,IAAMJ,EAASC,EAAkBC,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3CF,EAAO,KAAK,QAASK,CAAM,EAC3BL,EAAO,OAAO,EAAG,gBAAmB,CAClCA,EAAO,IAAI,QAASK,CAAM,EAC1BC,EAAQ,CACV,CAAC,CACH,CAAC,EACMH,EAAmBH,CAAM,CAClC,CAEA,SAASC,EAAkB,EAA0B,CACnD,IAAMM,EAAWV,EAAK,QAAQK,CAAG,EACjC,OAAOJ,EAAK,cAAc,EAAS,IAAa,CAC9C,IAAIU,EACJ,GAAI,CACF,IAAMC,EAAkB,IAAI,IAAIC,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmBD,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACNE,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEIH,IAAa,MACf,EAAW,eAGb,IAAMI,EAAwBJ,EAAS,SAAS,GAAG,EAAIX,EAAK,KAAKW,EAAU,YAAY,EAAIA,EAErFK,EAAWhB,EAAK,QAAQU,EAAU,IAAIK,GAAuB,EAC7DE,EAAejB,EAAK,SAASU,EAAUM,CAAQ,EACrD,GAAIC,IAAiB,MAAQA,EAAa,WAAW,KAAKjB,EAAK,KAAK,EAAG,CACrEc,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEA,GAAIf,EAAG,WAAWiB,CAAQ,EACxB,GAAI,CACF,IAAME,EAASnB,EAAG,aAAaiB,CAAQ,EACvCF,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgBK,EAAeJ,CAAqB,CACtD,CAAC,EACDD,EAAS,IAAII,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtBJ,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtBA,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAASR,EAAmB,EAAiC,CAC3D,IAAMc,EAAUjB,EAAO,QAAQ,EAC/B,GAAI,CAACiB,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjCjB,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnCA,EAAO,UAAY,CACjBM,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAOW,GAAY,SAAWA,EAAU,oBAAoBA,EAAQ,OACzE,KAAM,OAAOA,GAAY,SAAWA,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAMC,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAASF,EAAe,EAA0B,CAChD,IAAK,GAAM,CAACG,EAAQC,KAAgB,OAAO,QAAQF,CAAsB,EACvE,GAAIV,EAAS,SAASW,CAAM,EAAG,OAAOC,EAExC,MAAO,YACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdioJudgeRules.cjs","names":[],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"aACA,MAGa,EAA+B"}
1
+ {"version":3,"file":"stdioJudgeRules.cjs","names":["EXAMPLE_TEST_CASE_ID_PATTERN"],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"aACA,MAGaA,EAA+B"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdioJudgeRules.js","names":[],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"AACA,MAAa,EAAoB,IAGpB,EAA+B"}
1
+ {"version":3,"file":"stdioJudgeRules.js","names":["MAX_STDOUT_LENGTH","EXAMPLE_TEST_CASE_ID_PATTERN"],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"AACA,MAAaA,EAAoB,IAGpBC,EAA+B"}
@@ -1 +1 @@
1
- {"version":3,"file":"temporaryProblemDirCopy.cjs","names":["fs","path","os"],"sources":["../../src/helpers/temporaryProblemDirCopy.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n problemDir: string,\n options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\n try {\n const absoluteProblemDir = path.resolve(problemDir);\n const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n recursive: true,\n filter: rejectAbsoluteSymlinks,\n // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n // source tree, so judging the copy could write through them into the checked repository.\n verbatimSymlinks: true,\n });\n await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n return { tempRoot, copiedProblemDir };\n } catch (error) {\n await forciblyRemoveDirectory(tempRoot);\n throw error;\n }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n if (!isCopiedProblemPath(src)) return false;\n const stats = await fs.promises.lstat(src);\n if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n }\n return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n const name = path.basename(src);\n return name !== 'node_modules' && name !== '.git';\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n let currentDir = path.resolve(problemDir);\n while (true) {\n const nodeModulesPath = path.join(currentDir, 'node_modules');\n if (fs.existsSync(nodeModulesPath)) {\n const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n try {\n await fs.promises.symlink(\n nodeModulesPath,\n targetSymlinkPath,\n process.platform === 'win32' ? 'junction' : 'dir'\n );\n } catch {\n // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n const { root } = path.parse(absolutePath);\n return path.relative(root, absolutePath);\n}\n"],"mappings":"uPASA,eAAsB,EACpB,EACA,EAGI,CAAC,EACoD,CACzD,IAAM,EAAW,MAAMA,EAAAA,QAAG,SAAS,QAAQC,EAAAA,QAAK,KAAKC,EAAAA,QAAG,OAAO,EAAG,0BAA0B,CAAC,EAC7F,EAAQ,oBAAoB,CAAQ,EACpC,GAAI,CACF,IAAM,EAAqBD,EAAAA,QAAK,QAAQ,CAAU,EAC5C,EAAmBA,EAAAA,QAAK,KAAK,EAAU,EAAmB,CAAkB,CAAC,EAUnF,OATA,MAAMD,EAAAA,QAAG,SAAS,MAAMC,EAAAA,QAAK,QAAQ,CAAgB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3E,MAAMD,EAAAA,QAAG,SAAS,GAAG,EAAoB,EAAkB,CACzD,UAAW,GACX,OAAQ,EAGR,iBAAkB,EACpB,CAAC,EACD,MAAM,EAA8B,EAAU,CAAkB,EACzD,CAAE,WAAU,kBAAiB,CACtC,OAAS,EAAO,CAEd,MADA,MAAM,EAAwB,CAAQ,EAChC,CACR,CACF,CAMA,eAAsB,EAAwB,EAA+B,CAC3E,GAAI,CAEF,OADA,MAAMA,EAAAA,QAAG,SAAS,GAAG,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,EAAkB,CAAG,EACrB,GAAI,CAEF,OADA,MAAMA,EAAAA,QAAG,SAAS,GAAG,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAGA,SAAgB,EAA4B,EAAsB,CAChE,GAAI,CAEF,OADA,EAAA,QAAG,OAAO,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,EAAkB,CAAG,EACrB,GAAI,CAEF,OADA,EAAA,QAAG,OAAO,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAIA,SAAS,EAAkB,EAAmB,CACxC,QAAQ,WAAa,SAAS,EAAA,QAAc,UAAU,QAAS,CAAC,KAAM,QAAS,CAAG,CAAC,CACzF,CAIA,eAAe,EAAuB,EAA+B,CACnE,GAAI,CAAC,EAAoB,CAAG,EAAG,MAAO,GAEtC,IAAI,MADgBA,EAAAA,QAAG,SAAS,MAAM,CAAG,EAAA,CAC/B,eAAe,GAAKC,EAAAA,QAAK,WAAW,MAAMD,EAAAA,QAAG,SAAS,SAAS,CAAG,CAAC,EAC3E,MAAU,MAAM,GAAG,EAAI,uFAAuF,EAEhH,MAAO,EACT,CAEA,SAAS,EAAoB,EAAsB,CACjD,IAAM,EAAOC,EAAAA,QAAK,SAAS,CAAG,EAC9B,OAAO,IAAS,gBAAkB,IAAS,MAC7C,CAEA,eAAe,EAA8B,EAAkB,EAAmC,CAChG,IAAI,EAAaA,EAAAA,QAAK,QAAQ,CAAU,EACxC,OAAa,CACX,IAAM,EAAkBA,EAAAA,QAAK,KAAK,EAAY,cAAc,EAC5D,GAAID,EAAAA,QAAG,WAAW,CAAe,EAAG,CAClC,IAAM,EAAoBC,EAAAA,QAAK,KAAK,EAAU,EAAmB,CAAU,EAAG,cAAc,EAC5F,GAAI,CACF,MAAMD,EAAAA,QAAG,SAAS,QAChB,EACA,EACA,QAAQ,WAAa,QAAU,WAAa,KAC9C,CACF,MAAQ,CAER,CACF,CAEA,IAAM,EAAYC,EAAAA,QAAK,QAAQ,CAAU,EACzC,GAAI,IAAc,EAAY,MAC9B,EAAa,CACf,CACF,CAEA,SAAS,EAAmB,EAA8B,CACxD,GAAM,CAAE,QAASA,EAAAA,QAAK,MAAM,CAAY,EACxC,OAAOA,EAAAA,QAAK,SAAS,EAAM,CAAY,CACzC"}
1
+ {"version":3,"file":"temporaryProblemDirCopy.cjs","names":["fs","path","os","require_runtime","node_fs","node_path","node_os","node_child_process","copyProblemDirToTemporaryRoot","tempRoot","options","absoluteProblemDir","problemDir","copiedProblemDir","toTempRelativePath","rejectAbsoluteSymlinks","symlinkAllAncestorNodeModules","error","forciblyRemoveDirectory","dir","unlockPermissions","forciblyRemoveDirectorySync","isCopiedProblemPath","src","name","currentDir","nodeModulesPath","targetSymlinkPath","parentDir","absolutePath","root"],"sources":["../../src/helpers/temporaryProblemDirCopy.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n problemDir: string,\n options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\n try {\n const absoluteProblemDir = path.resolve(problemDir);\n const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n recursive: true,\n filter: rejectAbsoluteSymlinks,\n // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n // source tree, so judging the copy could write through them into the checked repository.\n verbatimSymlinks: true,\n });\n await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n return { tempRoot, copiedProblemDir };\n } catch (error) {\n await forciblyRemoveDirectory(tempRoot);\n throw error;\n }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n if (!isCopiedProblemPath(src)) return false;\n const stats = await fs.promises.lstat(src);\n if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n }\n return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n const name = path.basename(src);\n return name !== 'node_modules' && name !== '.git';\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n let currentDir = path.resolve(problemDir);\n while (true) {\n const nodeModulesPath = path.join(currentDir, 'node_modules');\n if (fs.existsSync(nodeModulesPath)) {\n const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n try {\n await fs.promises.symlink(\n nodeModulesPath,\n targetSymlinkPath,\n process.platform === 'win32' ? 'junction' : 'dir'\n );\n } catch {\n // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n const { root } = path.parse(absolutePath);\n return path.relative(root, absolutePath);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"temporaryProblemDirCopy.js","names":[],"sources":["../../src/helpers/temporaryProblemDirCopy.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n problemDir: string,\n options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\n try {\n const absoluteProblemDir = path.resolve(problemDir);\n const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n recursive: true,\n filter: rejectAbsoluteSymlinks,\n // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n // source tree, so judging the copy could write through them into the checked repository.\n verbatimSymlinks: true,\n });\n await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n return { tempRoot, copiedProblemDir };\n } catch (error) {\n await forciblyRemoveDirectory(tempRoot);\n throw error;\n }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n if (!isCopiedProblemPath(src)) return false;\n const stats = await fs.promises.lstat(src);\n if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n }\n return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n const name = path.basename(src);\n return name !== 'node_modules' && name !== '.git';\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n let currentDir = path.resolve(problemDir);\n while (true) {\n const nodeModulesPath = path.join(currentDir, 'node_modules');\n if (fs.existsSync(nodeModulesPath)) {\n const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n try {\n await fs.promises.symlink(\n nodeModulesPath,\n targetSymlinkPath,\n process.platform === 'win32' ? 'junction' : 'dir'\n );\n } catch {\n // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n const { root } = path.parse(absolutePath);\n return path.relative(root, absolutePath);\n}\n"],"mappings":"yGASA,eAAsB,EACpB,EACA,EAGI,CAAC,EACoD,CACzD,IAAM,EAAW,MAAM,EAAG,SAAS,QAAQ,EAAK,KAAK,EAAG,OAAO,EAAG,0BAA0B,CAAC,EAC7F,EAAQ,oBAAoB,CAAQ,EACpC,GAAI,CACF,IAAM,EAAqB,EAAK,QAAQ,CAAU,EAC5C,EAAmB,EAAK,KAAK,EAAU,EAAmB,CAAkB,CAAC,EAUnF,OATA,MAAM,EAAG,SAAS,MAAM,EAAK,QAAQ,CAAgB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3E,MAAM,EAAG,SAAS,GAAG,EAAoB,EAAkB,CACzD,UAAW,GACX,OAAQ,EAGR,iBAAkB,EACpB,CAAC,EACD,MAAM,EAA8B,EAAU,CAAkB,EACzD,CAAE,WAAU,kBAAiB,CACtC,OAAS,EAAO,CAEd,MADA,MAAM,EAAwB,CAAQ,EAChC,CACR,CACF,CAMA,eAAsB,EAAwB,EAA+B,CAC3E,GAAI,CAEF,OADA,MAAM,EAAG,SAAS,GAAG,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,EAAkB,CAAG,EACrB,GAAI,CAEF,OADA,MAAM,EAAG,SAAS,GAAG,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAGA,SAAgB,EAA4B,EAAsB,CAChE,GAAI,CAEF,OADA,EAAG,OAAO,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,EAAkB,CAAG,EACrB,GAAI,CAEF,OADA,EAAG,OAAO,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAIA,SAAS,EAAkB,EAAmB,CACxC,QAAQ,WAAa,SAAS,EAAc,UAAU,QAAS,CAAC,KAAM,QAAS,CAAG,CAAC,CACzF,CAIA,eAAe,EAAuB,EAA+B,CACnE,GAAI,CAAC,EAAoB,CAAG,EAAG,MAAO,GAEtC,IAAI,MADgB,EAAG,SAAS,MAAM,CAAG,EAAA,CAC/B,eAAe,GAAK,EAAK,WAAW,MAAM,EAAG,SAAS,SAAS,CAAG,CAAC,EAC3E,MAAU,MAAM,GAAG,EAAI,uFAAuF,EAEhH,MAAO,EACT,CAEA,SAAS,EAAoB,EAAsB,CACjD,IAAM,EAAO,EAAK,SAAS,CAAG,EAC9B,OAAO,IAAS,gBAAkB,IAAS,MAC7C,CAEA,eAAe,EAA8B,EAAkB,EAAmC,CAChG,IAAI,EAAa,EAAK,QAAQ,CAAU,EACxC,OAAa,CACX,IAAM,EAAkB,EAAK,KAAK,EAAY,cAAc,EAC5D,GAAI,EAAG,WAAW,CAAe,EAAG,CAClC,IAAM,EAAoB,EAAK,KAAK,EAAU,EAAmB,CAAU,EAAG,cAAc,EAC5F,GAAI,CACF,MAAM,EAAG,SAAS,QAChB,EACA,EACA,QAAQ,WAAa,QAAU,WAAa,KAC9C,CACF,MAAQ,CAER,CACF,CAEA,IAAM,EAAY,EAAK,QAAQ,CAAU,EACzC,GAAI,IAAc,EAAY,MAC9B,EAAa,CACf,CACF,CAEA,SAAS,EAAmB,EAA8B,CACxD,GAAM,CAAE,QAAS,EAAK,MAAM,CAAY,EACxC,OAAO,EAAK,SAAS,EAAM,CAAY,CACzC"}
1
+ {"version":3,"file":"temporaryProblemDirCopy.js","names":["fs","path","os","childProcess","copyProblemDirToTemporaryRoot","tempRoot","options","absoluteProblemDir","problemDir","copiedProblemDir","toTempRelativePath","rejectAbsoluteSymlinks","symlinkAllAncestorNodeModules","error","forciblyRemoveDirectory","dir","unlockPermissions","forciblyRemoveDirectorySync","isCopiedProblemPath","src","name","currentDir","nodeModulesPath","targetSymlinkPath","parentDir","absolutePath","root"],"sources":["../../src/helpers/temporaryProblemDirCopy.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n problemDir: string,\n options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\n try {\n const absoluteProblemDir = path.resolve(problemDir);\n const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n recursive: true,\n filter: rejectAbsoluteSymlinks,\n // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n // source tree, so judging the copy could write through them into the checked repository.\n verbatimSymlinks: true,\n });\n await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n return { tempRoot, copiedProblemDir };\n } catch (error) {\n await forciblyRemoveDirectory(tempRoot);\n throw error;\n }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n if (!isCopiedProblemPath(src)) return false;\n const stats = await fs.promises.lstat(src);\n if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n }\n return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n const name = path.basename(src);\n return name !== 'node_modules' && name !== '.git';\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n let currentDir = path.resolve(problemDir);\n while (true) {\n const nodeModulesPath = path.join(currentDir, 'node_modules');\n if (fs.existsSync(nodeModulesPath)) {\n const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n try {\n await fs.promises.symlink(\n nodeModulesPath,\n targetSymlinkPath,\n process.platform === 'win32' ? 'junction' : 'dir'\n );\n } catch {\n // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n const { root } = path.parse(absolutePath);\n return path.relative(root, absolutePath);\n}\n"],"mappings":"yGASA,eAAsBI,EACpB,EACA,EAGI,CAAC,EACoD,CACzD,IAAMC,EAAW,MAAML,EAAG,SAAS,QAAQC,EAAK,KAAKC,EAAG,OAAO,EAAG,0BAA0B,CAAC,EAC7FI,EAAQ,oBAAoBD,CAAQ,EACpC,GAAI,CACF,IAAME,EAAqBN,EAAK,QAAQO,CAAU,EAC5CC,EAAmBR,EAAK,KAAKI,EAAUK,EAAmBH,CAAkB,CAAC,EAUnF,OATA,MAAMP,EAAG,SAAS,MAAMC,EAAK,QAAQQ,CAAgB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3E,MAAMT,EAAG,SAAS,GAAGO,EAAoBE,EAAkB,CACzD,UAAW,GACX,OAAQE,EAGR,iBAAkB,EACpB,CAAC,EACD,MAAMC,EAA8BP,EAAUE,CAAkB,EACzD,CAAE,WAAU,kBAAiB,CACtC,OAASM,EAAO,CAEd,MADA,MAAMC,EAAwBT,CAAQ,EAChCQ,CACR,CACF,CAMA,eAAsBC,EAAwB,EAA+B,CAC3E,GAAI,CAEF,OADA,MAAMd,EAAG,SAAS,GAAGe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACNC,EAAkBD,CAAG,EACrB,GAAI,CAEF,OADA,MAAMf,EAAG,SAAS,GAAGe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAGA,SAAgBE,EAA4B,EAAsB,CAChE,GAAI,CAEF,OADAjB,EAAG,OAAOe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACNC,EAAkBD,CAAG,EACrB,GAAI,CAEF,OADAf,EAAG,OAAOe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAIA,SAASC,EAAkB,EAAmB,CACxC,QAAQ,WAAa,SAASb,EAAc,UAAU,QAAS,CAAC,KAAM,QAASY,CAAG,CAAC,CACzF,CAIA,eAAeJ,EAAuB,EAA+B,CACnE,GAAI,CAACO,EAAoBC,CAAG,EAAG,MAAO,GAEtC,IAAI,MADgBnB,EAAG,SAAS,MAAMmB,CAAG,EAAA,CAC/B,eAAe,GAAKlB,EAAK,WAAW,MAAMD,EAAG,SAAS,SAASmB,CAAG,CAAC,EAC3E,MAAU,MAAM,GAAGA,EAAI,uFAAuF,EAEhH,MAAO,EACT,CAEA,SAASD,EAAoB,EAAsB,CACjD,IAAME,EAAOnB,EAAK,SAASkB,CAAG,EAC9B,OAAOC,IAAS,gBAAkBA,IAAS,MAC7C,CAEA,eAAeR,EAA8B,EAAkB,EAAmC,CAChG,IAAIS,EAAapB,EAAK,QAAQO,CAAU,EACxC,OAAa,CACX,IAAMc,EAAkBrB,EAAK,KAAKoB,EAAY,cAAc,EAC5D,GAAIrB,EAAG,WAAWsB,CAAe,EAAG,CAClC,IAAMC,EAAoBtB,EAAK,KAAKI,EAAUK,EAAmBW,CAAU,EAAG,cAAc,EAC5F,GAAI,CACF,MAAMrB,EAAG,SAAS,QAChBsB,EACAC,EACA,QAAQ,WAAa,QAAU,WAAa,KAC9C,CACF,MAAQ,CAER,CACF,CAEA,IAAMC,EAAYvB,EAAK,QAAQoB,CAAU,EACzC,GAAIG,IAAcH,EAAY,MAC9B,EAAaG,CACf,CACF,CAEA,SAASd,EAAmB,EAA8B,CACxD,GAAM,CAAE,QAAST,EAAK,MAAMwB,CAAY,EACxC,OAAOxB,EAAK,SAASyB,EAAMD,CAAY,CACzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.cjs","names":["parseYaml"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"mCAMA,MAAM,EAAoB,4EACpB,EAA2B,2BAMjC,SAAgB,EAAiB,EAAqD,CACpF,IAAM,EAAQ,EAAkB,KAAK,CAAI,EACzC,GAAI,CAAC,GAAS,EAAM,QAAU,EAAG,CAG/B,GAAI,EAAyB,KAAK,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAM,CAAK,CACtC,CACA,MAAO,CAAE,YAAA,EAAYA,EAAAA,MAAAA,CAAU,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAM,EAAK,MAAM,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F"}
1
+ {"version":3,"file":"frontmatter.cjs","names":["parseYaml","yaml","FRONTMATTER_REGEX","FRONTMATTER_OPENER_REGEX","parseFrontmatter","match","text"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"mCAMA,MAAME,EAAoB,4EACpBC,EAA2B,2BAMjC,SAAgBC,EAAiB,EAAqD,CACpF,IAAMC,EAAQH,EAAkB,KAAKI,CAAI,EACzC,GAAI,CAACD,GAASA,EAAM,QAAU,EAAG,CAG/B,GAAIF,EAAyB,KAAKG,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAMA,CAAK,CACtC,CACA,MAAO,CAAE,YAAA,EAAYN,EAAAA,MAAAA,CAAUK,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAMC,EAAK,MAAMD,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F"}
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.js","names":["parseYaml"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"6BAMA,MAAM,EAAoB,4EACpB,EAA2B,2BAMjC,SAAgB,EAAiB,EAAqD,CACpF,IAAM,EAAQ,EAAkB,KAAK,CAAI,EACzC,GAAI,CAAC,GAAS,EAAM,QAAU,EAAG,CAG/B,GAAI,EAAyB,KAAK,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAM,CAAK,CACtC,CACA,MAAO,CAAE,WAAYA,EAAU,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAM,EAAK,MAAM,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F"}
1
+ {"version":3,"file":"frontmatter.js","names":["parseYaml","FRONTMATTER_REGEX","FRONTMATTER_OPENER_REGEX","parseFrontmatter","match","text","parse"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"6BAMA,MAAMC,EAAoB,4EACpBC,EAA2B,2BAMjC,SAAgBC,EAAiB,EAAqD,CACpF,IAAMC,EAAQH,EAAkB,KAAKI,CAAI,EACzC,GAAI,CAACD,GAASA,EAAM,QAAU,EAAG,CAG/B,GAAIF,EAAyB,KAAKG,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAMA,CAAK,CACtC,CACA,MAAO,CAAE,WAAYL,EAAUI,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAMC,EAAK,MAAMD,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F"}
@@ -1 +1 @@
1
- {"version":3,"file":"fsHelpers.cjs","names":["stat","lstat","readdir","basename","relative","join","LEARNING_MATERIAL_ID_REGEX","extname","readFile"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fsHelpers.cjs","names":["stat","lstat","readdir","basename","relative","join","LEARNING_MATERIAL_ID_REGEX","extname","readFile","require_schemas","node_path","node_fs_promises","IGNORED_DIRECTORY_NAMES","NON_CODE_EXTENSIONS","isFile","path","isRegularFile","isRegularDirectory","isDirectory","collectProblemDefinitions","definitionPathsById","collectProblemDefinitionsInto","rootDirectoryPath","dirents","directoryPath","dirent","problemId","definitionPaths","isExcludedFromDiscovery","directoryName","parentDirectoryPath","reportProblemDefinitionIssues","errors","isUsableProblemsDirectory","problemsDirectoryPath","collectProblemIdsFromDirectory","reportDanglingProblemReferences","location","problemIds","availableProblemIds","readSourceFilesRecursively","files","collectSourceFiles","f1","f2","entryPath"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fsHelpers.js","names":[],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fsHelpers.js","names":["IGNORED_DIRECTORY_NAMES","NON_CODE_EXTENSIONS","isFile","stat","path","isRegularFile","lstat","isRegularDirectory","isDirectory","collectProblemDefinitions","definitionPathsById","collectProblemDefinitionsInto","rootDirectoryPath","dirents","readdir","directoryPath","dirent","problemId","basename","definitionPaths","relative","join","isExcludedFromDiscovery","directoryName","parentDirectoryPath","reportProblemDefinitionIssues","LEARNING_MATERIAL_ID_REGEX","errors","isUsableProblemsDirectory","problemsDirectoryPath","collectProblemIdsFromDirectory","reportDanglingProblemReferences","location","problemIds","availableProblemIds","readSourceFilesRecursively","files","collectSourceFiles","f1","f2","entryPath","extname","readFile"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.cjs","names":["z","languageIdToDefinition","codeRuleSchema"],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { languageIdToDefinition } from '../types/language.js';\nimport { codeRuleSchema } from '../types/problem.js';\n\n// Schemas in this file mirror the Exercode judge importer so that generated materials validate\n// exactly like the importer would parse them.\n\nexport const LEARNING_MATERIAL_ID_REGEX = /^[0-9_a-z-]+$/;\n\nexport const learningMaterialIdSchema = z.string().regex(LEARNING_MATERIAL_ID_REGEX);\n\nexport const availableLanguageIds = Object.keys(languageIdToDefinition);\nconst availableLanguageIdSchema = z.enum(availableLanguageIds);\n\nconst environmentVariableNameSchema = z.string().regex(/^\\w+$/);\n\n// Mirrors the judge server's strict problem frontmatter schema; keys the server removed with the\n// v1 judge (judgeEnvironmentId, generalJudgeEnvironmentConfigOverrides, testCases, isGui) are\n// rejected here as unknown keys as well.\nexport const problemFrontmatterSchema = z.strictObject({\n name: z.string().min(1),\n type: z.enum(['prompt_study']).optional(),\n timeLimitMs: z.number().int().min(0).optional(),\n memoryLimitByte: z.number().int().min(0).optional(),\n requiredRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenTextsInCode: codeRuleSchema.array().default([]),\n canCreateFiles: z.boolean().optional(),\n isEditorDisabled: z.boolean().optional(),\n isAttachedFileRequired: z.boolean().optional(),\n isManualScoringRequired: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n requiredEnvironmentVariables: z.array(environmentVariableNameSchema).default([]),\n requiredOutputFilePaths: z.string().min(1).array().default([]),\n requiredSubmissionFilePaths: z.string().min(1).array().default([]),\n});\n\nexport type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;\n\nconst materialConfigShape = {\n availableLanguageIds: availableLanguageIdSchema.array().optional(),\n areTestCasesHidden: z.boolean().optional(),\n isProblemGradingResultHidden: z.boolean().optional(),\n isAutoFormatDisabled: z.boolean().optional(),\n isCopyAndPasteDisabled: z.boolean().optional(),\n isDebugHintDisabled: z.boolean().optional(),\n isFixHintDisabled: z.boolean().optional(),\n isDiffHintDisabled: z.boolean().optional(),\n debugHintWaitingSeconds: z.number().min(0).optional(),\n fixHintWaitingSeconds: z.number().min(0).optional(),\n diffHintWaitingSeconds: z.number().min(0).optional(),\n // z.iso.datetime is the zod v4 API (this repo pins zod 4); z.string().datetime is the v3 form.\n submissionOpenedAt: z.iso.datetime({ offset: true }).optional(),\n submissionSoftClosedAt: z.iso.datetime({ offset: true }).optional(),\n submissionHardClosedAt: z.iso.datetime({ offset: true }).optional(),\n isAutoTranslationDisabled: z.boolean().optional(),\n isModelAnswerShownAfterDeadline: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n isMaterialChatDisabled: z.boolean().optional(),\n} as const;\n\nexport const courseFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string(),\n author: z.string().min(1).optional(),\n isMotivationFeatureEnabled: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n // The importer strips unknown keys of a lecture entry (plain object) while the course object is strict.\n lectures: z\n .array(\n z.object({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n description: z.string(),\n })\n )\n .optional(),\n});\n\nexport type CourseFile = z.infer<typeof courseFileSchema>;\n\nconst uniqueOptionsSchema = z\n .string()\n .array()\n .min(1)\n // zod 4 names the custom-message option `error` (`message` is the deprecated zod 3 spelling).\n .refine((options) => options.length === new Set(options).size, { error: 'options must contain unique values' });\n\n// A quoted index is coerced like the judge does, but a non-numeric string is a schema error rather\n// than a NaN that would surface as an out-of-bounds index.\nconst answerIndexStringSchema = z\n .string()\n .regex(/^\\d+$/, { error: 'must be a non-negative integer index (quoted numbers such as \"0\" are accepted)' })\n .transform(Number);\n\nconst questionBaseShape = {\n id: learningMaterialIdSchema,\n isResubmittable: z.boolean().optional(),\n isSurvey: z.boolean().optional(),\n explanation: z.string().optional(),\n hint: z.string().optional(),\n} as const;\n\nconst selectQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select'),\n options: uniqueOptionsSchema,\n answerIndex: z\n .union([\n z.number().int().nonnegative(),\n answerIndexStringSchema,\n z.number().int().nonnegative().array(),\n answerIndexStringSchema.array(),\n ])\n .optional(),\n} as const;\n\nconst selectMultipleQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select_multiple'),\n options: uniqueOptionsSchema,\n answerIndices: z.union([z.number().int().nonnegative().array(), answerIndexStringSchema.array()]).optional(),\n} as const;\n\nconst textQuestionShape = {\n ...questionBaseShape,\n type: z.literal('text'),\n answerPattern: z.string().min(1).optional(),\n modelAnswer: z.string().optional(),\n} as const;\n\nexport const questionInCodeBlockSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string() }),\n z.strictObject({ ...textQuestionShape, question: z.string() }),\n]);\n\n// The judge still supports question definitions in material frontmatter where `question` is optional.\nconst questionInFrontmatterSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...textQuestionShape, question: z.string().optional() }),\n]);\n\nexport type MaterialQuestion = z.infer<typeof questionInFrontmatterSchema>;\n\nexport const materialFrontmatterSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n problems: z\n .object({ id: learningMaterialIdSchema, courseId: learningMaterialIdSchema.optional() })\n .array()\n .optional(),\n questions: z.array(questionInFrontmatterSchema).optional(),\n turtleGraphicsQuestions: z.object({ id: learningMaterialIdSchema }).array().optional(),\n isExamination: z.boolean().optional(),\n isMockExamination: z.boolean().optional(),\n isRealtimeSurvey: z.boolean().optional(),\n});\n\nexport const CONTEST_MATERIAL_FILE_SUFFIX = '.contest.yaml';\n\nconst contestDivisionSchema = z\n .strictObject({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n openedAt: z.iso.datetime({ offset: true }),\n closedAt: z.iso.datetime({ offset: true }),\n password: z.string().min(1).optional(),\n })\n .refine((division) => new Date(division.openedAt) < new Date(division.closedAt), {\n error: 'openedAt must be earlier than closedAt',\n });\n\nconst contestProblemSchema = z.strictObject({\n id: learningMaterialIdSchema,\n score: z.number().int().nonnegative(),\n courseId: learningMaterialIdSchema.optional(),\n // Per-problem overrides reuse the shared material-config fields, mirroring the judge schema.\n areTestCasesHidden: materialConfigShape.areTestCasesHidden,\n isDebugHintDisabled: materialConfigShape.isDebugHintDisabled,\n isFixHintDisabled: materialConfigShape.isFixHintDisabled,\n isDiffHintDisabled: materialConfigShape.isDiffHintDisabled,\n debugHintWaitingSeconds: materialConfigShape.debugHintWaitingSeconds,\n fixHintWaitingSeconds: materialConfigShape.fixHintWaitingSeconds,\n diffHintWaitingSeconds: materialConfigShape.diffHintWaitingSeconds,\n isMaterialChatDisabled: materialConfigShape.isMaterialChatDisabled,\n});\n\nexport const contestFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string().optional(),\n // Exercode reads the contest administrators itself and strips the key before the judge's strict schema.\n adminEmails: z.array(z.string().trim().email()).optional(),\n showsProblemsAfterClose: z.boolean().optional(),\n divisions: z.array(contestDivisionSchema).min(1),\n problems: z.array(contestProblemSchema).min(1),\n});\n"],"mappings":"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"}
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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","names":[],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { languageIdToDefinition } from '../types/language.js';\nimport { codeRuleSchema } from '../types/problem.js';\n\n// Schemas in this file mirror the Exercode judge importer so that generated materials validate\n// exactly like the importer would parse them.\n\nexport const LEARNING_MATERIAL_ID_REGEX = /^[0-9_a-z-]+$/;\n\nexport const learningMaterialIdSchema = z.string().regex(LEARNING_MATERIAL_ID_REGEX);\n\nexport const availableLanguageIds = Object.keys(languageIdToDefinition);\nconst availableLanguageIdSchema = z.enum(availableLanguageIds);\n\nconst environmentVariableNameSchema = z.string().regex(/^\\w+$/);\n\n// Mirrors the judge server's strict problem frontmatter schema; keys the server removed with the\n// v1 judge (judgeEnvironmentId, generalJudgeEnvironmentConfigOverrides, testCases, isGui) are\n// rejected here as unknown keys as well.\nexport const problemFrontmatterSchema = z.strictObject({\n name: z.string().min(1),\n type: z.enum(['prompt_study']).optional(),\n timeLimitMs: z.number().int().min(0).optional(),\n memoryLimitByte: z.number().int().min(0).optional(),\n requiredRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenTextsInCode: codeRuleSchema.array().default([]),\n canCreateFiles: z.boolean().optional(),\n isEditorDisabled: z.boolean().optional(),\n isAttachedFileRequired: z.boolean().optional(),\n isManualScoringRequired: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n requiredEnvironmentVariables: z.array(environmentVariableNameSchema).default([]),\n requiredOutputFilePaths: z.string().min(1).array().default([]),\n requiredSubmissionFilePaths: z.string().min(1).array().default([]),\n});\n\nexport type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;\n\nconst materialConfigShape = {\n availableLanguageIds: availableLanguageIdSchema.array().optional(),\n areTestCasesHidden: z.boolean().optional(),\n isProblemGradingResultHidden: z.boolean().optional(),\n isAutoFormatDisabled: z.boolean().optional(),\n isCopyAndPasteDisabled: z.boolean().optional(),\n isDebugHintDisabled: z.boolean().optional(),\n isFixHintDisabled: z.boolean().optional(),\n isDiffHintDisabled: z.boolean().optional(),\n debugHintWaitingSeconds: z.number().min(0).optional(),\n fixHintWaitingSeconds: z.number().min(0).optional(),\n diffHintWaitingSeconds: z.number().min(0).optional(),\n // z.iso.datetime is the zod v4 API (this repo pins zod 4); z.string().datetime is the v3 form.\n submissionOpenedAt: z.iso.datetime({ offset: true }).optional(),\n submissionSoftClosedAt: z.iso.datetime({ offset: true }).optional(),\n submissionHardClosedAt: z.iso.datetime({ offset: true }).optional(),\n isAutoTranslationDisabled: z.boolean().optional(),\n isModelAnswerShownAfterDeadline: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n isMaterialChatDisabled: z.boolean().optional(),\n} as const;\n\nexport const courseFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string(),\n author: z.string().min(1).optional(),\n isMotivationFeatureEnabled: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n // The importer strips unknown keys of a lecture entry (plain object) while the course object is strict.\n lectures: z\n .array(\n z.object({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n description: z.string(),\n })\n )\n .optional(),\n});\n\nexport type CourseFile = z.infer<typeof courseFileSchema>;\n\nconst uniqueOptionsSchema = z\n .string()\n .array()\n .min(1)\n // zod 4 names the custom-message option `error` (`message` is the deprecated zod 3 spelling).\n .refine((options) => options.length === new Set(options).size, { error: 'options must contain unique values' });\n\n// A quoted index is coerced like the judge does, but a non-numeric string is a schema error rather\n// than a NaN that would surface as an out-of-bounds index.\nconst answerIndexStringSchema = z\n .string()\n .regex(/^\\d+$/, { error: 'must be a non-negative integer index (quoted numbers such as \"0\" are accepted)' })\n .transform(Number);\n\nconst questionBaseShape = {\n id: learningMaterialIdSchema,\n isResubmittable: z.boolean().optional(),\n isSurvey: z.boolean().optional(),\n explanation: z.string().optional(),\n hint: z.string().optional(),\n} as const;\n\nconst selectQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select'),\n options: uniqueOptionsSchema,\n answerIndex: z\n .union([\n z.number().int().nonnegative(),\n answerIndexStringSchema,\n z.number().int().nonnegative().array(),\n answerIndexStringSchema.array(),\n ])\n .optional(),\n} as const;\n\nconst selectMultipleQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select_multiple'),\n options: uniqueOptionsSchema,\n answerIndices: z.union([z.number().int().nonnegative().array(), answerIndexStringSchema.array()]).optional(),\n} as const;\n\nconst textQuestionShape = {\n ...questionBaseShape,\n type: z.literal('text'),\n answerPattern: z.string().min(1).optional(),\n modelAnswer: z.string().optional(),\n} as const;\n\nexport const questionInCodeBlockSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string() }),\n z.strictObject({ ...textQuestionShape, question: z.string() }),\n]);\n\n// The judge still supports question definitions in material frontmatter where `question` is optional.\nconst questionInFrontmatterSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...textQuestionShape, question: z.string().optional() }),\n]);\n\nexport type MaterialQuestion = z.infer<typeof questionInFrontmatterSchema>;\n\nexport const materialFrontmatterSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n problems: z\n .object({ id: learningMaterialIdSchema, courseId: learningMaterialIdSchema.optional() })\n .array()\n .optional(),\n questions: z.array(questionInFrontmatterSchema).optional(),\n turtleGraphicsQuestions: z.object({ id: learningMaterialIdSchema }).array().optional(),\n isExamination: z.boolean().optional(),\n isMockExamination: z.boolean().optional(),\n isRealtimeSurvey: z.boolean().optional(),\n});\n\nexport const CONTEST_MATERIAL_FILE_SUFFIX = '.contest.yaml';\n\nconst contestDivisionSchema = z\n .strictObject({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n openedAt: z.iso.datetime({ offset: true }),\n closedAt: z.iso.datetime({ offset: true }),\n password: z.string().min(1).optional(),\n })\n .refine((division) => new Date(division.openedAt) < new Date(division.closedAt), {\n error: 'openedAt must be earlier than closedAt',\n });\n\nconst contestProblemSchema = z.strictObject({\n id: learningMaterialIdSchema,\n score: z.number().int().nonnegative(),\n courseId: learningMaterialIdSchema.optional(),\n // Per-problem overrides reuse the shared material-config fields, mirroring the judge schema.\n areTestCasesHidden: materialConfigShape.areTestCasesHidden,\n isDebugHintDisabled: materialConfigShape.isDebugHintDisabled,\n isFixHintDisabled: materialConfigShape.isFixHintDisabled,\n isDiffHintDisabled: materialConfigShape.isDiffHintDisabled,\n debugHintWaitingSeconds: materialConfigShape.debugHintWaitingSeconds,\n fixHintWaitingSeconds: materialConfigShape.fixHintWaitingSeconds,\n diffHintWaitingSeconds: materialConfigShape.diffHintWaitingSeconds,\n isMaterialChatDisabled: materialConfigShape.isMaterialChatDisabled,\n});\n\nexport const contestFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string().optional(),\n // Exercode reads the contest administrators itself and strips the key before the judge's strict schema.\n adminEmails: z.array(z.string().trim().email()).optional(),\n showsProblemsAfterClose: z.boolean().optional(),\n divisions: z.array(contestDivisionSchema).min(1),\n problems: z.array(contestProblemSchema).min(1),\n});\n"],"mappings":"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"}
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 +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"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport {\n mergeSubmissionPeriods,\n reportForeignProblemCourseIds,\n validateSubmissionPeriods,\n type SubmissionPeriods,\n} from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface ContestValidationOptions {\n /** When set, every contest problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n /** Set by course validation, which already warns once about a missing problems directory. */\n isNestedInCourse?: boolean;\n}\n\n/** Validates a judge contest material (`*.contest.yaml`) file. */\nexport async function validateContestFile(\n contestFilePath: string,\n options: ContestValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // The judge lists lecture entries as regular files, so a symbolic link is skipped at import time.\n if (!(await isRegularFile(contestFilePath))) {\n errors.push(\n (await isFile(contestFilePath))\n ? `contest file is a symbolic link, which the judge ignores; commit a regular file: ${contestFilePath}`\n : `contest file not found: ${contestFilePath}`\n );\n return result;\n }\n const fileName = basename(contestFilePath);\n if (!fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n errors.push(`contest file name must end with ${CONTEST_MATERIAL_FILE_SUFFIX}: ${fileName}`);\n return result;\n }\n const contestId = fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length);\n if (!LEARNING_MATERIAL_ID_REGEX.test(contestId)) {\n errors.push(\n `contest ID \"${contestId}\" (file name without ${CONTEST_MATERIAL_FILE_SUFFIX}) must match ${LEARNING_MATERIAL_ID_REGEX}`\n );\n }\n\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(contestFilePath, 'utf8'));\n } catch (error) {\n errors.push(`invalid YAML: ${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":"mQA4BA,eAAsB,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAM,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACtB,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAMA,EAAAA,cAAc,CAAe,EAMvC,OALA,EAAO,KACJ,MAAMC,EAAAA,OAAO,CAAe,EACzB,oFAAoF,IACpF,2BAA2B,GACjC,EACO,EAET,IAAM,GAAA,EAAWC,EAAAA,SAAAA,CAAS,CAAe,EACzC,GAAI,CAAC,EAAS,SAAA,eAAqC,EAEjD,OADA,EAAO,KAAK,mCAAmCC,EAAAA,6BAA6B,IAAI,GAAU,EACnF,EAET,IAAM,EAAY,EAAS,MAAM,EAAG,CAACA,EAAAA,6BAA6B,MAAM,EACnEC,EAAAA,2BAA2B,KAAK,CAAS,GAC5C,EAAO,KACL,eAAe,EAAU,uBAAuBD,EAAAA,6BAA6B,eAAeC,EAAAA,4BAC9F,EAGF,IAAI,EACJ,GAAI,CACF,GAAA,EAAaC,EAAAA,MAAAA,CAAU,MAAA,EAAMC,EAAAA,SAAAA,CAAS,EAAiB,MAAM,CAAC,CAChE,OAAS,EAAO,CAEd,OADA,EAAO,KAAK,kBAAA,EAAiBC,EAAAA,gBAAAA,CAAgB,CAAK,GAAG,EAC9C,CACT,CACA,IAAM,EAASC,EAAAA,kBAAkB,UAAU,CAAU,EACrD,GAAI,CAAC,EAAO,QAEV,OADA,EAAO,KAAK,GAAGC,EAAAA,gBAAgB,EAAO,MAAO,SAAS,CAAC,EAChD,EAGT,EAAA,0BAA0BC,EAAAA,uBAAuB,EAAO,KAAM,EAAQ,uBAAuB,EAAG,EAAQ,EAAO,IAAI,EACnH,EAAA,8BAA8B,EAAO,KAAK,SAAU,EAAQ,SAAU,CAAM,EAC5E,EAAA,mBACE,EAAO,KAAK,UAAU,IAAK,GAAa,EAAS,EAAE,EACnD,WACA,CACF,EACA,EAAA,mBACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,UACA,CACF,EAEA,IAAI,EAAsB,EAAQ,oBAkBlC,OAjBI,IAAwB,IAAA,KACtB,EAAQ,wBAA0B,IAAA,GAC/B,EAAQ,kBACX,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAMC,EAAAA,+BAA+B,EAAQ,sBAAuB,CAAM,GAGhG,IAAwB,IAAA,IAC1B,EAAA,gCACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,EACA,EAAQ,sBACR,CACF,EAEK,CACT"}
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';\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport {\n mergeSubmissionPeriods,\n reportForeignProblemCourseIds,\n validateSubmissionPeriods,\n type SubmissionPeriods,\n} from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface ContestValidationOptions {\n /** When set, every contest problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n /** Set by course validation, which already warns once about a missing problems directory. */\n isNestedInCourse?: boolean;\n}\n\n/** Validates a judge contest material (`*.contest.yaml`) file. */\nexport async function validateContestFile(\n contestFilePath: string,\n options: ContestValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // The judge lists lecture entries as regular files, so a symbolic link is skipped at import time.\n if (!(await isRegularFile(contestFilePath))) {\n errors.push(\n (await isFile(contestFilePath))\n ? `contest file is a symbolic link, which the judge ignores; commit a regular file: ${contestFilePath}`\n : `contest file not found: ${contestFilePath}`\n );\n return result;\n }\n const fileName = basename(contestFilePath);\n if (!fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n errors.push(`contest file name must end with ${CONTEST_MATERIAL_FILE_SUFFIX}: ${fileName}`);\n return result;\n }\n const contestId = fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length);\n if (!LEARNING_MATERIAL_ID_REGEX.test(contestId)) {\n errors.push(\n `contest ID \"${contestId}\" (file name without ${CONTEST_MATERIAL_FILE_SUFFIX}) must match ${LEARNING_MATERIAL_ID_REGEX}`\n );\n }\n\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(contestFilePath, 'utf8'));\n } catch (error) {\n errors.push(`invalid YAML: ${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":"mQA4BA,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 +1 @@
1
- {"version":3,"file":"validateContest.js","names":["parseYaml"],"sources":["../../src/learningMaterial/validateContest.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport {\n mergeSubmissionPeriods,\n reportForeignProblemCourseIds,\n validateSubmissionPeriods,\n type SubmissionPeriods,\n} from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface ContestValidationOptions {\n /** When set, every contest problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n /** Set by course validation, which already warns once about a missing problems directory. */\n isNestedInCourse?: boolean;\n}\n\n/** Validates a judge contest material (`*.contest.yaml`) file. */\nexport async function validateContestFile(\n contestFilePath: string,\n options: ContestValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // The judge lists lecture entries as regular files, so a symbolic link is skipped at import time.\n if (!(await isRegularFile(contestFilePath))) {\n errors.push(\n (await isFile(contestFilePath))\n ? `contest file is a symbolic link, which the judge ignores; commit a regular file: ${contestFilePath}`\n : `contest file not found: ${contestFilePath}`\n );\n return result;\n }\n const fileName = basename(contestFilePath);\n if (!fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n errors.push(`contest file name must end with ${CONTEST_MATERIAL_FILE_SUFFIX}: ${fileName}`);\n return result;\n }\n const contestId = fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length);\n if (!LEARNING_MATERIAL_ID_REGEX.test(contestId)) {\n errors.push(\n `contest ID \"${contestId}\" (file name without ${CONTEST_MATERIAL_FILE_SUFFIX}) must match ${LEARNING_MATERIAL_ID_REGEX}`\n );\n }\n\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(contestFilePath, 'utf8'));\n } catch (error) {\n errors.push(`invalid YAML: ${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":"gnBA4BA,eAAsB,EACpB,EACA,EAAoC,CAAC,EACV,CAC3B,IAAM,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACtB,EAAS,CAAE,SAAQ,UAAS,EAGlC,GAAI,CAAE,MAAM,EAAc,CAAe,EAMvC,OALA,EAAO,KACJ,MAAM,EAAO,CAAe,EACzB,oFAAoF,IACpF,2BAA2B,GACjC,EACO,EAET,IAAM,EAAW,EAAS,CAAe,EACzC,GAAI,CAAC,EAAS,SAAA,eAAqC,EAEjD,OADA,EAAO,KAAK,mCAAmC,EAA6B,IAAI,GAAU,EACnF,EAET,IAAM,EAAY,EAAS,MAAM,EAAG,CAAC,EAA6B,MAAM,EACnE,EAA2B,KAAK,CAAS,GAC5C,EAAO,KACL,eAAe,EAAU,uBAAuB,EAA6B,eAAe,GAC9F,EAGF,IAAI,EACJ,GAAI,CACF,EAAaA,EAAU,MAAM,EAAS,EAAiB,MAAM,CAAC,CAChE,OAAS,EAAO,CAEd,OADA,EAAO,KAAK,iBAAiB,EAAgB,CAAK,GAAG,EAC9C,CACT,CACA,IAAM,EAAS,EAAkB,UAAU,CAAU,EACrD,GAAI,CAAC,EAAO,QAEV,OADA,EAAO,KAAK,GAAG,EAAgB,EAAO,MAAO,SAAS,CAAC,EAChD,EAGT,EAA0B,EAAuB,EAAO,KAAM,EAAQ,uBAAuB,EAAG,EAAQ,EAAO,IAAI,EACnH,EAA8B,EAAO,KAAK,SAAU,EAAQ,SAAU,CAAM,EAC5E,EACE,EAAO,KAAK,UAAU,IAAK,GAAa,EAAS,EAAE,EACnD,WACA,CACF,EACA,EACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,UACA,CACF,EAEA,IAAI,EAAsB,EAAQ,oBAkBlC,OAjBI,IAAwB,IAAA,KACtB,EAAQ,wBAA0B,IAAA,GAC/B,EAAQ,kBACX,EAAS,KAAK,uFAAuF,EAGvG,EAAsB,MAAM,EAA+B,EAAQ,sBAAuB,CAAM,GAGhG,IAAwB,IAAA,IAC1B,EACE,EAAO,KAAK,SAAS,IAAK,GAAY,EAAQ,EAAE,EAChD,EACA,EAAQ,sBACR,CACF,EAEK,CACT"}
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';\nimport { getErrorMessage } from '@willbooster/shared-lib';\nimport { parse as parseYaml } from 'yaml';\nimport { collectProblemIdsFromDirectory, isFile, isRegularFile, reportDanglingProblemReferences } from './fsHelpers.js';\nimport { CONTEST_MATERIAL_FILE_SUFFIX, contestFileSchema, LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\nimport {\n mergeSubmissionPeriods,\n reportForeignProblemCourseIds,\n validateSubmissionPeriods,\n type SubmissionPeriods,\n} from './validateMaterial.js';\nimport { formatZodIssues, reportDuplicateIds, type ValidationResult } from './validationResult.js';\n\nexport interface ContestValidationOptions {\n /** When set, every contest problem ID must be a problem discovered under this path. */\n problemsDirectoryPath?: string;\n /** Problem IDs already collected for the enclosing course; collected from `problemsDirectoryPath` otherwise. */\n availableProblemIds?: ReadonlySet<string>;\n /** Enclosing course ID; the judge rejects problem references whose `courseId` differs from it. */\n courseId?: string;\n /** Course-level submission periods; the judge merges them as `material[field] ?? course[field]`. */\n courseSubmissionPeriods?: SubmissionPeriods;\n /** Set by course validation, which already warns once about a missing problems directory. */\n isNestedInCourse?: boolean;\n}\n\n/** Validates a judge contest material (`*.contest.yaml`) file. */\nexport async function validateContestFile(\n contestFilePath: string,\n options: ContestValidationOptions = {}\n): Promise<ValidationResult> {\n const errors: string[] = [];\n const warnings: string[] = [];\n const result = { errors, warnings };\n\n // The judge lists lecture entries as regular files, so a symbolic link is skipped at import time.\n if (!(await isRegularFile(contestFilePath))) {\n errors.push(\n (await isFile(contestFilePath))\n ? `contest file is a symbolic link, which the judge ignores; commit a regular file: ${contestFilePath}`\n : `contest file not found: ${contestFilePath}`\n );\n return result;\n }\n const fileName = basename(contestFilePath);\n if (!fileName.endsWith(CONTEST_MATERIAL_FILE_SUFFIX)) {\n errors.push(`contest file name must end with ${CONTEST_MATERIAL_FILE_SUFFIX}: ${fileName}`);\n return result;\n }\n const contestId = fileName.slice(0, -CONTEST_MATERIAL_FILE_SUFFIX.length);\n if (!LEARNING_MATERIAL_ID_REGEX.test(contestId)) {\n errors.push(\n `contest ID \"${contestId}\" (file name without ${CONTEST_MATERIAL_FILE_SUFFIX}) must match ${LEARNING_MATERIAL_ID_REGEX}`\n );\n }\n\n let rawContent: unknown;\n try {\n rawContent = parseYaml(await readFile(contestFilePath, 'utf8'));\n } catch (error) {\n errors.push(`invalid YAML: ${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":"gnBA4BA,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"}