@exercode/problem-utils 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/checkAllProblems.cjs +1 -1
  3. package/dist/cli/checkAllProblems.js +1 -1
  4. package/dist/cli/exercodeProblem.cjs +8 -2
  5. package/dist/cli/exercodeProblem.cjs.map +1 -1
  6. package/dist/cli/exercodeProblem.js +8 -2
  7. package/dist/cli/exercodeProblem.js.map +1 -1
  8. package/dist/cli/validateLearningMaterial.cjs +2 -0
  9. package/dist/cli/validateLearningMaterial.cjs.map +1 -0
  10. package/dist/cli/validateLearningMaterial.d.ts +7 -0
  11. package/dist/cli/validateLearningMaterial.js +2 -0
  12. package/dist/cli/validateLearningMaterial.js.map +1 -0
  13. package/dist/helpers/judgeByStaticAnalysis.cjs +3 -3
  14. package/dist/helpers/judgeByStaticAnalysis.js +3 -3
  15. package/dist/helpers/stdioJudgeRules.cjs +2 -0
  16. package/dist/helpers/stdioJudgeRules.cjs.map +1 -0
  17. package/dist/helpers/stdioJudgeRules.d.ts +4 -0
  18. package/dist/helpers/stdioJudgeRules.js +2 -0
  19. package/dist/helpers/stdioJudgeRules.js.map +1 -0
  20. package/dist/index.cjs +1 -1
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.js +1 -1
  23. package/dist/learningMaterial/frontmatter.cjs +2 -0
  24. package/dist/learningMaterial/frontmatter.cjs.map +1 -0
  25. package/dist/learningMaterial/frontmatter.d.ts +8 -0
  26. package/dist/learningMaterial/frontmatter.js +2 -0
  27. package/dist/learningMaterial/frontmatter.js.map +1 -0
  28. package/dist/learningMaterial/fsHelpers.cjs +2 -0
  29. package/dist/learningMaterial/fsHelpers.cjs.map +1 -0
  30. package/dist/learningMaterial/fsHelpers.d.ts +30 -0
  31. package/dist/learningMaterial/fsHelpers.js +2 -0
  32. package/dist/learningMaterial/fsHelpers.js.map +1 -0
  33. package/dist/learningMaterial/schemas.cjs +2 -0
  34. package/dist/learningMaterial/schemas.cjs.map +1 -0
  35. package/dist/learningMaterial/schemas.d.ts +241 -0
  36. package/dist/learningMaterial/schemas.js +2 -0
  37. package/dist/learningMaterial/schemas.js.map +1 -0
  38. package/dist/learningMaterial/validateContest.cjs +2 -0
  39. package/dist/learningMaterial/validateContest.cjs.map +1 -0
  40. package/dist/learningMaterial/validateContest.d.ts +16 -0
  41. package/dist/learningMaterial/validateContest.js +2 -0
  42. package/dist/learningMaterial/validateContest.js.map +1 -0
  43. package/dist/learningMaterial/validateCourse.cjs +2 -0
  44. package/dist/learningMaterial/validateCourse.cjs.map +1 -0
  45. package/dist/learningMaterial/validateCourse.d.ts +11 -0
  46. package/dist/learningMaterial/validateCourse.js +2 -0
  47. package/dist/learningMaterial/validateCourse.js.map +1 -0
  48. package/dist/learningMaterial/validateMaterial.cjs +4 -0
  49. package/dist/learningMaterial/validateMaterial.cjs.map +1 -0
  50. package/dist/learningMaterial/validateMaterial.d.ts +34 -0
  51. package/dist/learningMaterial/validateMaterial.js +4 -0
  52. package/dist/learningMaterial/validateMaterial.js.map +1 -0
  53. package/dist/learningMaterial/validateProblem.cjs +2 -0
  54. package/dist/learningMaterial/validateProblem.cjs.map +1 -0
  55. package/dist/learningMaterial/validateProblem.d.ts +6 -0
  56. package/dist/learningMaterial/validateProblem.js +2 -0
  57. package/dist/learningMaterial/validateProblem.js.map +1 -0
  58. package/dist/learningMaterial/validationResult.cjs +3 -0
  59. package/dist/learningMaterial/validationResult.cjs.map +1 -0
  60. package/dist/learningMaterial/validationResult.d.ts +9 -0
  61. package/dist/learningMaterial/validationResult.js +3 -0
  62. package/dist/learningMaterial/validationResult.js.map +1 -0
  63. package/dist/presets/command.cjs +1 -1
  64. package/dist/presets/command.js +1 -1
  65. package/dist/presets/evaluation.cjs +2 -2
  66. package/dist/presets/evaluation.js +2 -2
  67. package/dist/presets/guiCommand.cjs +1 -1
  68. package/dist/presets/guiCommand.js +1 -1
  69. package/dist/presets/stdio.cjs +1 -1
  70. package/dist/presets/stdio.cjs.map +1 -1
  71. package/dist/presets/stdio.js +1 -1
  72. package/dist/presets/stdio.js.map +1 -1
  73. package/package.json +2 -1
package/README.md CHANGED
@@ -31,6 +31,29 @@ The all-problem check judges serially by default because time limits are measure
31
31
 
32
32
  A standard stdin/stdout problem must NOT commit a `judge.ts` or `debug.ts` that is identical to the default stdio harness: the absence of `judge.ts` marks the problem as standard, and committed copies would drift from the server's defaults. The CLI rejects such files; a file kept intentionally (e.g. to demonstrate the default harness) can add an explanatory comment to be treated as custom.
33
33
 
34
+ ## Validators
35
+
36
+ The CLI also validates learning-material files without running any program, mirroring the checks the Exercode importer applies:
37
+
38
+ ```bash
39
+ # Validate problem directories (problem.md frontmatter, test_cases, model_answers, templates, judge.ts / debug.ts).
40
+ bun x exercode-problem validate-problem <problemDir>...
41
+ # Validate a course directory (course.yaml, lecture materials with embedded questions, problem references).
42
+ bun x exercode-problem validate-course <courseDir> [--problems-dir <dir>]
43
+ # Validate a contest (*.contest.yaml) file.
44
+ bun x exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]
45
+ ```
46
+
47
+ Each target prints `OK` or `NG` followed by its errors and warnings; the command exits 1 when any target has an error. `--problems-dir` points to the directory holding the referenced problems; a course is always searched at any depth, since Exercode links a material only to the problems inside its course, so for a course the option must name a directory inside it. The validators are also exported (`validateProblemDirectory`, `validateCourseDirectory`, `validateContestFile`, `validateMaterialFile`).
48
+
49
+ ## Agent skills
50
+
51
+ The [`skills/`](skills/) directory holds skills for AI coding agents that author and review Exercode learning content: `generate-learning-content` (entry point), `generate-course-materials`, `generate-judge-problems`, `generate-judge-contest`, `review-learning-content`, and `setup-exercode-course-repository`. Agents working in this repository load them through the symlinks under `.claude/skills/`; install them elsewhere with the [skills](https://github.com/vercel-labs/skills) CLI:
52
+
53
+ ```bash
54
+ bun x skills add WillBooster/exercode-problem-utils --agent claude-code --agent codex
55
+ ```
56
+
34
57
  ## Test cases
35
58
 
36
59
  A problem keeps its test cases under `test_cases/`. A test case id is the shared name of the following entries, and each entry is optional:
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../helpers/defaultStdioHarness.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../helpers/temporaryProblemDirCopy.cjs"),a=require("../types/decisionCode.cjs"),o=require("../helpers/runHarnessProcess.cjs"),s=require("../helpers/readProblemMarkdownFrontMatter.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("./runSingleHarness.cjs");let u=require("node:path");u=e.__toESM(u,1);let d=require("node:fs/promises");d=e.__toESM(d,1);const f=1e3,p=new Map(Object.entries(a.DecisionCode).map(([e,t])=>[t,e]));async function m(e){let t=_(e),i=u.default.resolve(t.rootDir),a=e=>(u.default.relative(i,e)||`.`).replaceAll(u.default.sep,`/`),o=await v(i),d=o.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(d.length===0)return console.error(o.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${o.length} problem directories under ${i} were excluded by --only/--skip.`),1;let f=[],p=[];for(let e of d){let t=await n.findDefaultStdioHarnessFiles(e);if(t.length>0){f.push(`${a(e)}: ${l.formatDefaultHarnessError(t)}`);continue}let i=await c.findModelAnswerDirs(e);if(i.length===0){f.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await b(u.default.join(e,`judge.ts`))&&(await r.readTestCases(u.default.join(e,`test_cases`))).length===0){let t;try{t=await s.readProblemMarkdownFrontMatter(e)}catch(t){f.push(`${a(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!s.judgesWithoutTestCases(t)){f.push(`${a(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let o=await c.findFailingModelAnswerDirs(e);p.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...o.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of f)console.error(`✗ ${e}`);let m=u.default.resolve(process.argv[1]??``),g=0,y=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t.concurrency,p.length))},async()=>{for(;y<p.length;){let e=p[y++];if(!e)return;let t=`${a(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await h(e,m);n===void 0?(g++,console.info(`✓ ${t}`)):(f.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${g} passed, ${f.length} failed (${p.length} runs, ${d.length} problems)`),f.length===0?0:1}async function h(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return x(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let a,s;try{a=g(e,await o.runHarnessProcess([`run`,t,`judge`,u.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){a=x(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{s=await i.forciblyRemoveDirectory(n)}if(s)return a;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?c:x(`${a}; ${c}`)}function g(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return x(n.failureReason);if(n.exitCode!==0)return x(`harness exited with ${n.exitCode??`a signal`}${i.trim()?`: ${i.trim()}`:``}`);let o=r.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of o){let n;try{n=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)))}catch{n=void 0}if(!n?.success)return x(`malformed test case result line: ${e}`);s.push(n.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==a.DecisionCode.ACCEPTED);return e?x(`${p.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===a.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function _(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function v(e){let t=[];return await y(e,t),t.toSorted()}async function y(e,t){let n=await d.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await y(u.default.join(e,r.name),t)}async function b(e){try{return await d.default.stat(e),!0}catch{return!1}}function x(e){return e.length<=f?e:`${e.slice(0,f)}...`}exports.checkAllProblems=m;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../helpers/defaultStdioHarness.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../helpers/temporaryProblemDirCopy.cjs"),a=require("../helpers/readProblemMarkdownFrontMatter.cjs"),o=require("../types/decisionCode.cjs"),s=require("../helpers/runHarnessProcess.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("./runSingleHarness.cjs");let u=require("node:path");u=e.__toESM(u,1);let d=require("node:fs/promises");d=e.__toESM(d,1);const f=1e3,p=new Map(Object.entries(o.DecisionCode).map(([e,t])=>[t,e]));async function m(e){let t=_(e),i=u.default.resolve(t.rootDir),o=e=>(u.default.relative(i,e)||`.`).replaceAll(u.default.sep,`/`),s=await v(i),d=s.filter(e=>{let n=o(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(d.length===0)return console.error(s.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${s.length} problem directories under ${i} were excluded by --only/--skip.`),1;let f=[],p=[];for(let e of d){let t=await n.findDefaultStdioHarnessFiles(e);if(t.length>0){f.push(`${o(e)}: ${l.formatDefaultHarnessError(t)}`);continue}let i=await c.findModelAnswerDirs(e);if(i.length===0){f.push(`${o(e)}: no model answers found under model_answers/`);continue}if(!await b(u.default.join(e,`judge.ts`))&&(await r.readTestCases(u.default.join(e,`test_cases`))).length===0){let t;try{t=await a.readProblemMarkdownFrontMatter(e)}catch(t){f.push(`${o(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!a.judgesWithoutTestCases(t)){f.push(`${o(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let s=await c.findFailingModelAnswerDirs(e);p.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...s.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of f)console.error(`✗ ${e}`);let m=u.default.resolve(process.argv[1]??``),g=0,y=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t.concurrency,p.length))},async()=>{for(;y<p.length;){let e=p[y++];if(!e)return;let t=`${o(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await h(e,m);n===void 0?(g++,console.info(`✓ ${t}`)):(f.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${g} passed, ${f.length} failed (${p.length} runs, ${d.length} problems)`),f.length===0?0:1}async function h(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return x(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let a,o;try{a=g(e,await s.runHarnessProcess([`run`,t,`judge`,u.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){a=x(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{o=await i.forciblyRemoveDirectory(n)}if(o)return a;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?c:x(`${a}; ${c}`)}function g(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return x(n.failureReason);if(n.exitCode!==0)return x(`harness exited with ${n.exitCode??`a signal`}${i.trim()?`: ${i.trim()}`:``}`);let a=r.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of a){let n;try{n=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)))}catch{n=void 0}if(!n?.success)return x(`malformed test case result line: ${e}`);s.push(n.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==o.DecisionCode.ACCEPTED);return e?x(`${p.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===o.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function _(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function v(e){let t=[];return await y(e,t),t.toSorted()}async function y(e,t){let n=await d.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await y(u.default.join(e,r.name),t)}async function b(e){try{return await d.default.stat(e),!0}catch{return!1}}function x(e){return e.length<=f?e:`${e.slice(0,f)}...`}exports.checkAllProblems=m;
2
2
  //# sourceMappingURL=checkAllProblems.cjs.map
@@ -1,2 +1,2 @@
1
- import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{findDefaultStdioHarnessFiles as n}from"../helpers/defaultStdioHarness.js";import{readTestCases as r}from"../helpers/readTestCases.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"../helpers/temporaryProblemDirCopy.js";import{DecisionCode as o}from"../types/decisionCode.js";import{runHarnessProcess as s}from"../helpers/runHarnessProcess.js";import{judgesWithoutTestCases as c,readProblemMarkdownFrontMatter as l}from"../helpers/readProblemMarkdownFrontMatter.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:path";import h from"node:fs/promises";const g=1e3,_=new Map(Object.entries(o).map(([e,t])=>[t,e]));async function v(e){let t=x(e),i=m.resolve(t.rootDir),a=e=>(m.relative(i,e)||`.`).replaceAll(m.sep,`/`),o=await S(i),s=o.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(s.length===0)return console.error(o.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${o.length} problem directories under ${i} were excluded by --only/--skip.`),1;let h=[],g=[];for(let e of s){let t=await n(e);if(t.length>0){h.push(`${a(e)}: ${p(t)}`);continue}let i=await d(e);if(i.length===0){h.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await w(m.join(e,`judge.ts`))&&(await r(m.join(e,`test_cases`))).length===0){let t;try{t=await l(e)}catch(t){h.push(`${a(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!c(t)){h.push(`${a(e)}: ${f}`);continue}}let o=await u(e);g.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...o.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of h)console.error(`✗ ${e}`);let _=m.resolve(process.argv[1]??``),v=0,b=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t.concurrency,g.length))},async()=>{for(;b<g.length;){let e=g[b++];if(!e)return;let t=`${a(e.problemDir)} ${m.relative(e.problemDir,e.answerDir).replaceAll(m.sep,`/`)}`,n=await y(e,_);n===void 0?(v++,console.info(`✓ ${t}`)):(h.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${v} passed, ${h.length} failed (${g.length} runs, ${s.length} problems)`),h.length===0?0:1}async function y(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return T(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let o,c;try{o=b(e,await s([`run`,t,`judge`,m.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=T(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{c=await a(n)}if(c)return o;let l=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return o===void 0?l:T(`${o}; ${l}`)}function b(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return T(r.failureReason);if(r.exitCode!==0)return T(`harness exited with ${r.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let s=i.split(/\r?\n/).filter(t=>t.startsWith(e)),c=[];for(let n of s){let r;try{r=t.safeParse(JSON.parse(n.slice(e.length)))}catch{r=void 0}if(!r?.success)return T(`malformed test case result line: ${n}`);c.push(r.data)}if(c.length===0)return`no test case results were printed`;if(n.expectation===`accepted`){let e=c.find(e=>e.decisionCode!==o.ACCEPTED);return e?T(`${_.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return c.every(e=>e.decisionCode===o.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function x(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function S(e){let t=[];return await C(e,t),t.toSorted()}async function C(e,t){let n=await h.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await C(m.join(e,r.name),t)}async function w(e){try{return await h.stat(e),!0}catch{return!1}}function T(e){return e.length<=g?e:`${e.slice(0,g)}...`}export{v as checkAllProblems};
1
+ import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{findDefaultStdioHarnessFiles as n}from"../helpers/defaultStdioHarness.js";import{readTestCases as r}from"../helpers/readTestCases.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"../helpers/temporaryProblemDirCopy.js";import{judgesWithoutTestCases as o,readProblemMarkdownFrontMatter as s}from"../helpers/readProblemMarkdownFrontMatter.js";import{DecisionCode as c}from"../types/decisionCode.js";import{runHarnessProcess as l}from"../helpers/runHarnessProcess.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:path";import h from"node:fs/promises";const g=1e3,_=new Map(Object.entries(c).map(([e,t])=>[t,e]));async function v(e){let t=x(e),i=m.resolve(t.rootDir),a=e=>(m.relative(i,e)||`.`).replaceAll(m.sep,`/`),c=await S(i),l=c.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(l.length===0)return console.error(c.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${c.length} problem directories under ${i} were excluded by --only/--skip.`),1;let h=[],g=[];for(let e of l){let t=await n(e);if(t.length>0){h.push(`${a(e)}: ${p(t)}`);continue}let i=await d(e);if(i.length===0){h.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await w(m.join(e,`judge.ts`))&&(await r(m.join(e,`test_cases`))).length===0){let t;try{t=await s(e)}catch(t){h.push(`${a(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!o(t)){h.push(`${a(e)}: ${f}`);continue}}let c=await u(e);g.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...c.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of h)console.error(`✗ ${e}`);let _=m.resolve(process.argv[1]??``),v=0,b=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(t.concurrency,g.length))},async()=>{for(;b<g.length;){let e=g[b++];if(!e)return;let t=`${a(e.problemDir)} ${m.relative(e.problemDir,e.answerDir).replaceAll(m.sep,`/`)}`,n=await y(e,_);n===void 0?(v++,console.info(`✓ ${t}`)):(h.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${v} passed, ${h.length} failed (${g.length} runs, ${l.length} problems)`),h.length===0?0:1}async function y(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return T(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let o,s;try{o=b(e,await l([`run`,t,`judge`,m.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=T(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{s=await a(n)}if(s)return o;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return o===void 0?c:T(`${o}; ${c}`)}function b(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return T(r.failureReason);if(r.exitCode!==0)return T(`harness exited with ${r.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let o=i.split(/\r?\n/).filter(t=>t.startsWith(e)),s=[];for(let n of o){let r;try{r=t.safeParse(JSON.parse(n.slice(e.length)))}catch{r=void 0}if(!r?.success)return T(`malformed test case result line: ${n}`);s.push(r.data)}if(s.length===0)return`no test case results were printed`;if(n.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==c.ACCEPTED);return e?T(`${_.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===c.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function x(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function S(e){let t=[];return await C(e,t),t.toSorted()}async function C(e,t){let n=await h.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await C(m.join(e,r.name),t)}async function w(e){try{return await h.stat(e),!0}catch{return!1}}function T(e){return e.length<=g?e:`${e.slice(0,g)}...`}export{v as checkAllProblems};
2
2
  //# sourceMappingURL=checkAllProblems.js.map
@@ -1,9 +1,15 @@
1
- "use strict";const e=require("./runSingleHarness.cjs"),t=require("./checkAllProblems.cjs"),n=`Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]
1
+ "use strict";const e=require("./runSingleHarness.cjs"),t=require("./checkAllProblems.cjs"),n=require("./validateLearningMaterial.cjs"),r=`Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]
2
2
  exercode-problem judge <answerDir> [paramsJson]
3
3
  exercode-problem debug <answerDir> [paramsJson]
4
+ exercode-problem validate-problem <problemDir>...
5
+ exercode-problem validate-course <courseDir> [--problems-dir <dir>]
6
+ exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]
4
7
 
5
8
  Without a subcommand, judge all model answers of all problems under rootDir
6
9
  (default: the current directory). Use \`judge\` or \`debug\` in a problem
7
10
  directory to run one answer directory, e.g.:
8
- exercode-problem debug model_answers/python '{ "stdin": "1 2" }'`;r();async function r(){let r=process.argv.slice(2);if(r[0]===`--help`||r[0]===`-h`){console.info(n);return}try{if(r[0]===`judge`||r[0]===`debug`){if(r.length<2){console.error(n),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e.runSingleHarness(r[0])}else process.exitCode=await t.checkAllProblems(r)}catch(e){console.error(e),process.exitCode=1}}
11
+ exercode-problem debug model_answers/python '{ "stdin": "1 2" }'
12
+ The \`validate-*\` subcommands check problem directories, a course directory
13
+ (course.yaml and lecture materials), or a contest (*.contest.yaml) file
14
+ without running any program.`;i();async function i(){let i=process.argv.slice(2);if(i[0]===`--help`||i[0]===`-h`){console.info(r);return}try{if(i[0]===`judge`||i[0]===`debug`){if(i.length<2){console.error(r),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e.runSingleHarness(i[0])}else a(i[0])?process.exitCode=await n.validateLearningMaterial(i[0],i.slice(1)):process.exitCode=await t.checkAllProblems(i)}catch(e){console.error(e),process.exitCode=1}}function a(e){return n.VALIDATE_SUBCOMMANDS.includes(e??``)}
9
15
  //# sourceMappingURL=exercodeProblem.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"exercodeProblem.cjs","names":["runSingleHarness","checkAllProblems"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n"],"mappings":"2FAGM,EAAQ;;;;;;;oEAUT,EAAK,EAEV,eAAe,GAAsB,CACnC,IAAM,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAI,EAAK,KAAO,UAAY,EAAK,KAAO,KAAM,CAE5C,QAAQ,KAAK,CAAK,EAClB,MACF,CACA,GAAI,CACF,GAAI,EAAK,KAAO,SAAW,EAAK,KAAO,QAAS,CAC9C,GAAI,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMA,EAAAA,iBAAiB,EAAK,EAAE,CACnD,KACE,SAAQ,SAAW,MAAMC,EAAAA,iBAAiB,CAAI,CAElD,OAAS,EAAO,CAEd,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF"}
1
+ {"version":3,"file":"exercodeProblem.cjs","names":["runSingleHarness","validateLearningMaterial","checkAllProblems","VALIDATE_SUBCOMMANDS"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, type ValidateSubcommand, validateLearningMaterial } from './validateLearningMaterial.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n exercode-problem validate-problem <problemDir>...\n exercode-problem validate-course <courseDir> [--problems-dir <dir>]\n exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'\nThe \\`validate-*\\` subcommands check problem directories, a course directory\n(course.yaml and lecture materials), or a contest (*.contest.yaml) file\nwithout running any program.`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else if (isValidateSubcommand(args[0])) {\n process.exitCode = await validateLearningMaterial(args[0], args.slice(1));\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n\nfunction isValidateSubcommand(arg: string | undefined): arg is ValidateSubcommand {\n return (VALIDATE_SUBCOMMANDS as readonly string[]).includes(arg ?? '');\n}\n"],"mappings":"uIAIM,EAAQ;;;;;;;;;;;;;8BAgBT,EAAK,EAEV,eAAe,GAAsB,CACnC,IAAM,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAI,EAAK,KAAO,UAAY,EAAK,KAAO,KAAM,CAE5C,QAAQ,KAAK,CAAK,EAClB,MACF,CACA,GAAI,CACF,GAAI,EAAK,KAAO,SAAW,EAAK,KAAO,QAAS,CAC9C,GAAI,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMA,EAAAA,iBAAiB,EAAK,EAAE,CACnD,MAAW,EAAqB,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMC,EAAAA,yBAAyB,EAAK,GAAI,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMC,EAAAA,iBAAiB,CAAI,CAElD,OAAS,EAAO,CAEd,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAAS,EAAqB,EAAoD,CAChF,OAAQC,EAAAA,qBAA2C,SAAS,GAAO,EAAE,CACvE"}
@@ -1,9 +1,15 @@
1
- import{runSingleHarness as e}from"./runSingleHarness.js";import{checkAllProblems as t}from"./checkAllProblems.js";const n=`Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]
1
+ import{runSingleHarness as e}from"./runSingleHarness.js";import{checkAllProblems as t}from"./checkAllProblems.js";import{VALIDATE_SUBCOMMANDS as n,validateLearningMaterial as r}from"./validateLearningMaterial.js";const i=`Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]
2
2
  exercode-problem judge <answerDir> [paramsJson]
3
3
  exercode-problem debug <answerDir> [paramsJson]
4
+ exercode-problem validate-problem <problemDir>...
5
+ exercode-problem validate-course <courseDir> [--problems-dir <dir>]
6
+ exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]
4
7
 
5
8
  Without a subcommand, judge all model answers of all problems under rootDir
6
9
  (default: the current directory). Use \`judge\` or \`debug\` in a problem
7
10
  directory to run one answer directory, e.g.:
8
- exercode-problem debug model_answers/python '{ "stdin": "1 2" }'`;r();async function r(){let r=process.argv.slice(2);if(r[0]===`--help`||r[0]===`-h`){console.info(n);return}try{if(r[0]===`judge`||r[0]===`debug`){if(r.length<2){console.error(n),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e(r[0])}else process.exitCode=await t(r)}catch(e){console.error(e),process.exitCode=1}}
11
+ exercode-problem debug model_answers/python '{ "stdin": "1 2" }'
12
+ The \`validate-*\` subcommands check problem directories, a course directory
13
+ (course.yaml and lecture materials), or a contest (*.contest.yaml) file
14
+ without running any program.`;a();async function a(){let n=process.argv.slice(2);if(n[0]===`--help`||n[0]===`-h`){console.info(i);return}try{if(n[0]===`judge`||n[0]===`debug`){if(n.length<2){console.error(i),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e(n[0])}else o(n[0])?process.exitCode=await r(n[0],n.slice(1)):process.exitCode=await t(n)}catch(e){console.error(e),process.exitCode=1}}function o(e){return n.includes(e??``)}
9
15
  //# sourceMappingURL=exercodeProblem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exercodeProblem.js","names":[],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n"],"mappings":"kHAGA,MAAM,EAAQ;;;;;;;oEAUT,EAAK,EAEV,eAAe,GAAsB,CACnC,IAAM,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAI,EAAK,KAAO,UAAY,EAAK,KAAO,KAAM,CAE5C,QAAQ,KAAK,CAAK,EAClB,MACF,CACA,GAAI,CACF,GAAI,EAAK,KAAO,SAAW,EAAK,KAAO,QAAS,CAC9C,GAAI,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAM,EAAiB,EAAK,EAAE,CACnD,KACE,SAAQ,SAAW,MAAM,EAAiB,CAAI,CAElD,OAAS,EAAO,CAEd,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF"}
1
+ {"version":3,"file":"exercodeProblem.js","names":[],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, type ValidateSubcommand, validateLearningMaterial } from './validateLearningMaterial.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n exercode-problem validate-problem <problemDir>...\n exercode-problem validate-course <courseDir> [--problems-dir <dir>]\n exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'\nThe \\`validate-*\\` subcommands check problem directories, a course directory\n(course.yaml and lecture materials), or a contest (*.contest.yaml) file\nwithout running any program.`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else if (isValidateSubcommand(args[0])) {\n process.exitCode = await validateLearningMaterial(args[0], args.slice(1));\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n\nfunction isValidateSubcommand(arg: string | undefined): arg is ValidateSubcommand {\n return (VALIDATE_SUBCOMMANDS as readonly string[]).includes(arg ?? '');\n}\n"],"mappings":"qNAIA,MAAM,EAAQ;;;;;;;;;;;;;8BAgBT,EAAK,EAEV,eAAe,GAAsB,CACnC,IAAM,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAI,EAAK,KAAO,UAAY,EAAK,KAAO,KAAM,CAE5C,QAAQ,KAAK,CAAK,EAClB,MACF,CACA,GAAI,CACF,GAAI,EAAK,KAAO,SAAW,EAAK,KAAO,QAAS,CAC9C,GAAI,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAM,EAAiB,EAAK,EAAE,CACnD,MAAW,EAAqB,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAM,EAAyB,EAAK,GAAI,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAM,EAAiB,CAAI,CAElD,OAAS,EAAO,CAEd,QAAQ,MAAM,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAAS,EAAqB,EAAoD,CAChF,OAAQ,EAA2C,SAAS,GAAO,EAAE,CACvE"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../learningMaterial/validationResult.cjs"),t=require("../learningMaterial/validateContest.cjs"),n=require("../learningMaterial/validateCourse.cjs"),r=require("../learningMaterial/validateProblem.cjs"),i=[`validate-problem`,`validate-course`,`validate-contest`];async function a(t,n){let{targets:r,problemsDir:i}=s(n);if(r.length===0)throw Error(`${t} requires at least one path`);if(t===`validate-problem`){if(i!==void 0)throw Error(`${t} does not take --problems-dir`)}else if(r.length>1)throw Error(`${t} accepts exactly one path, but got ${r.length}`);let a=!1;for(let n of r){let r=await o(t,n,i);process.stdout.write(e.formatValidationReport(n,r)),a||=r.errors.length>0}return+!!a}async function o(e,i,a){switch(e){case`validate-problem`:return r.validateProblemDirectory(i);case`validate-course`:return n.validateCourseDirectory(i,{problemsDirectoryPath:a});case`validate-contest`:return t.validateContestFile(i,{problemsDirectoryPath:a})}}function s(e){let t=[],n;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--problems-dir`){if(n=e[++r],n===void 0)throw Error(`${i} requires a value`)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else t.push(i)}return{targets:t,problemsDir:n}}exports.VALIDATE_SUBCOMMANDS=i,exports.validateLearningMaterial=a;
2
+ //# sourceMappingURL=validateLearningMaterial.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateLearningMaterial.cjs","names":["formatValidationReport","validateProblemDirectory","validateCourseDirectory","validateContestFile"],"sources":["../../src/cli/validateLearningMaterial.ts"],"sourcesContent":["import { validateContestFile } from '../learningMaterial/validateContest.js';\nimport { validateCourseDirectory } from '../learningMaterial/validateCourse.js';\nimport { validateProblemDirectory } from '../learningMaterial/validateProblem.js';\nimport { formatValidationReport, type ValidationResult } from '../learningMaterial/validationResult.js';\n\nexport const VALIDATE_SUBCOMMANDS = ['validate-problem', 'validate-course', 'validate-contest'] as const;\n\nexport type ValidateSubcommand = (typeof VALIDATE_SUBCOMMANDS)[number];\n\n/**\n * Validate learning-material files without running any program and print one report per target.\n * Returns the process exit code: 1 when any target has an error (warnings alone pass).\n */\nexport async function validateLearningMaterial(\n subcommand: ValidateSubcommand,\n args: readonly string[]\n): Promise<number> {\n const { targets, problemsDir } = parseValidateArgs(args);\n if (targets.length === 0) throw new Error(`${subcommand} requires at least one path`);\n if (subcommand === 'validate-problem') {\n if (problemsDir !== undefined) throw new Error(`${subcommand} does not take --problems-dir`);\n } else if (targets.length > 1) {\n throw new Error(`${subcommand} accepts exactly one path, but got ${targets.length}`);\n }\n\n let hasError = false;\n for (const target of targets) {\n const result = await validate(subcommand, target, problemsDir);\n process.stdout.write(formatValidationReport(target, result));\n hasError ||= result.errors.length > 0;\n }\n return hasError ? 1 : 0;\n}\n\nasync function validate(\n subcommand: ValidateSubcommand,\n target: string,\n problemsDirectoryPath: string | undefined\n): Promise<ValidationResult> {\n switch (subcommand) {\n case 'validate-problem': {\n return validateProblemDirectory(target);\n }\n case 'validate-course': {\n return validateCourseDirectory(target, { problemsDirectoryPath });\n }\n case 'validate-contest': {\n return validateContestFile(target, { problemsDirectoryPath });\n }\n }\n}\n\nfunction parseValidateArgs(args: readonly string[]): { targets: string[]; problemsDir: string | undefined } {\n const targets: string[] = [];\n let problemsDir: string | undefined;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--problems-dir') {\n problemsDir = args[++index];\n if (problemsDir === undefined) throw new Error(`${arg} requires a value`);\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else {\n targets.push(arg);\n }\n }\n return { targets, problemsDir };\n}\n"],"mappings":"0SAKa,EAAuB,CAAC,mBAAoB,kBAAmB,kBAAkB,EAQ9F,eAAsB,EACpB,EACA,EACiB,CACjB,GAAM,CAAE,UAAS,eAAgB,EAAkB,CAAI,EACvD,GAAI,EAAQ,SAAW,EAAG,MAAU,MAAM,GAAG,EAAW,4BAA4B,EACpF,GAAI,IAAe,mBACb,IAAA,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAG,EAAW,8BAA8B,CAAA,MACtF,GAAI,EAAQ,OAAS,EAC1B,MAAU,MAAM,GAAG,EAAW,qCAAqC,EAAQ,QAAQ,EAGrF,IAAI,EAAW,GACf,IAAK,IAAM,KAAU,EAAS,CAC5B,IAAM,EAAS,MAAM,EAAS,EAAY,EAAQ,CAAW,EAC7D,QAAQ,OAAO,MAAMA,EAAAA,uBAAuB,EAAQ,CAAM,CAAC,EAC3D,IAAa,EAAO,OAAO,OAAS,CACtC,CACA,MAAO,IACT,CAEA,eAAe,EACb,EACA,EACA,EAC2B,CAC3B,OAAQ,EAAR,CACE,IAAK,mBACH,OAAOC,EAAAA,yBAAyB,CAAM,EAExC,IAAK,kBACH,OAAOC,EAAAA,wBAAwB,EAAQ,CAAE,uBAAsB,CAAC,EAElE,IAAK,mBACH,OAAOC,EAAAA,oBAAoB,EAAQ,CAAE,uBAAsB,CAAC,CAEhE,CACF,CAEA,SAAS,EAAkB,EAAiF,CAC1G,IAAM,EAAoB,CAAC,EACvB,EACJ,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,IAAS,CAChD,IAAM,EAAM,EAAK,GACjB,GAAI,IAAQ,IAAA,GAAW,MACvB,GAAI,IAAQ,iBAEV,IADA,EAAc,EAAK,EAAE,GACjB,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAG,EAAI,kBAAkB,CAAA,MACnE,GAAI,EAAI,WAAW,IAAI,EAC5B,MAAU,MAAM,mBAAmB,GAAK,OAExC,EAAQ,KAAK,CAAG,CAEpB,CACA,MAAO,CAAE,UAAS,aAAY,CAChC"}
@@ -0,0 +1,7 @@
1
+ export declare const VALIDATE_SUBCOMMANDS: readonly ['validate-problem', 'validate-course', 'validate-contest'];
2
+ export type ValidateSubcommand = (typeof VALIDATE_SUBCOMMANDS)[number];
3
+ /**
4
+ * Validate learning-material files without running any program and print one report per target.
5
+ * Returns the process exit code: 1 when any target has an error (warnings alone pass).
6
+ */
7
+ export declare function validateLearningMaterial(subcommand: ValidateSubcommand, args: readonly string[]): Promise<number>;
@@ -0,0 +1,2 @@
1
+ import{formatValidationReport as e}from"../learningMaterial/validationResult.js";import{validateContestFile as t}from"../learningMaterial/validateContest.js";import{validateCourseDirectory as n}from"../learningMaterial/validateCourse.js";import{validateProblemDirectory as r}from"../learningMaterial/validateProblem.js";const i=[`validate-problem`,`validate-course`,`validate-contest`];async function a(t,n){let{targets:r,problemsDir:i}=s(n);if(r.length===0)throw Error(`${t} requires at least one path`);if(t===`validate-problem`){if(i!==void 0)throw Error(`${t} does not take --problems-dir`)}else if(r.length>1)throw Error(`${t} accepts exactly one path, but got ${r.length}`);let a=!1;for(let n of r){let r=await o(t,n,i);process.stdout.write(e(n,r)),a||=r.errors.length>0}return+!!a}async function o(e,i,a){switch(e){case`validate-problem`:return r(i);case`validate-course`:return n(i,{problemsDirectoryPath:a});case`validate-contest`:return t(i,{problemsDirectoryPath:a})}}function s(e){let t=[],n;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--problems-dir`){if(n=e[++r],n===void 0)throw Error(`${i} requires a value`)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else t.push(i)}return{targets:t,problemsDir:n}}export{i as VALIDATE_SUBCOMMANDS,a as validateLearningMaterial};
2
+ //# sourceMappingURL=validateLearningMaterial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateLearningMaterial.js","names":[],"sources":["../../src/cli/validateLearningMaterial.ts"],"sourcesContent":["import { validateContestFile } from '../learningMaterial/validateContest.js';\nimport { validateCourseDirectory } from '../learningMaterial/validateCourse.js';\nimport { validateProblemDirectory } from '../learningMaterial/validateProblem.js';\nimport { formatValidationReport, type ValidationResult } from '../learningMaterial/validationResult.js';\n\nexport const VALIDATE_SUBCOMMANDS = ['validate-problem', 'validate-course', 'validate-contest'] as const;\n\nexport type ValidateSubcommand = (typeof VALIDATE_SUBCOMMANDS)[number];\n\n/**\n * Validate learning-material files without running any program and print one report per target.\n * Returns the process exit code: 1 when any target has an error (warnings alone pass).\n */\nexport async function validateLearningMaterial(\n subcommand: ValidateSubcommand,\n args: readonly string[]\n): Promise<number> {\n const { targets, problemsDir } = parseValidateArgs(args);\n if (targets.length === 0) throw new Error(`${subcommand} requires at least one path`);\n if (subcommand === 'validate-problem') {\n if (problemsDir !== undefined) throw new Error(`${subcommand} does not take --problems-dir`);\n } else if (targets.length > 1) {\n throw new Error(`${subcommand} accepts exactly one path, but got ${targets.length}`);\n }\n\n let hasError = false;\n for (const target of targets) {\n const result = await validate(subcommand, target, problemsDir);\n process.stdout.write(formatValidationReport(target, result));\n hasError ||= result.errors.length > 0;\n }\n return hasError ? 1 : 0;\n}\n\nasync function validate(\n subcommand: ValidateSubcommand,\n target: string,\n problemsDirectoryPath: string | undefined\n): Promise<ValidationResult> {\n switch (subcommand) {\n case 'validate-problem': {\n return validateProblemDirectory(target);\n }\n case 'validate-course': {\n return validateCourseDirectory(target, { problemsDirectoryPath });\n }\n case 'validate-contest': {\n return validateContestFile(target, { problemsDirectoryPath });\n }\n }\n}\n\nfunction parseValidateArgs(args: readonly string[]): { targets: string[]; problemsDir: string | undefined } {\n const targets: string[] = [];\n let problemsDir: string | undefined;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--problems-dir') {\n problemsDir = args[++index];\n if (problemsDir === undefined) throw new Error(`${arg} requires a value`);\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else {\n targets.push(arg);\n }\n }\n return { targets, problemsDir };\n}\n"],"mappings":"gUAKA,MAAa,EAAuB,CAAC,mBAAoB,kBAAmB,kBAAkB,EAQ9F,eAAsB,EACpB,EACA,EACiB,CACjB,GAAM,CAAE,UAAS,eAAgB,EAAkB,CAAI,EACvD,GAAI,EAAQ,SAAW,EAAG,MAAU,MAAM,GAAG,EAAW,4BAA4B,EACpF,GAAI,IAAe,mBACb,IAAA,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAG,EAAW,8BAA8B,CAAA,MACtF,GAAI,EAAQ,OAAS,EAC1B,MAAU,MAAM,GAAG,EAAW,qCAAqC,EAAQ,QAAQ,EAGrF,IAAI,EAAW,GACf,IAAK,IAAM,KAAU,EAAS,CAC5B,IAAM,EAAS,MAAM,EAAS,EAAY,EAAQ,CAAW,EAC7D,QAAQ,OAAO,MAAM,EAAuB,EAAQ,CAAM,CAAC,EAC3D,IAAa,EAAO,OAAO,OAAS,CACtC,CACA,MAAO,IACT,CAEA,eAAe,EACb,EACA,EACA,EAC2B,CAC3B,OAAQ,EAAR,CACE,IAAK,mBACH,OAAO,EAAyB,CAAM,EAExC,IAAK,kBACH,OAAO,EAAwB,EAAQ,CAAE,uBAAsB,CAAC,EAElE,IAAK,mBACH,OAAO,EAAoB,EAAQ,CAAE,uBAAsB,CAAC,CAEhE,CACF,CAEA,SAAS,EAAkB,EAAiF,CAC1G,IAAM,EAAoB,CAAC,EACvB,EACJ,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,IAAS,CAChD,IAAM,EAAM,EAAK,GACjB,GAAI,IAAQ,IAAA,GAAW,MACvB,GAAI,IAAQ,iBAEV,IADA,EAAc,EAAK,EAAE,GACjB,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAG,EAAI,kBAAkB,CAAA,MACnE,GAAI,EAAI,WAAW,IAAI,EAC5B,MAAU,MAAM,mBAAmB,GAAK,OAExC,EAAQ,KAAK,CAAG,CAEpB,CACA,MAAO,CAAE,UAAS,aAAY,CAChC"}
@@ -1,14 +1,14 @@
1
- "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./removeCommentsInSourceCode.cjs"),n=require("../types/decisionCode.cjs"),r=require("../types/problem.cjs"),i=require("./findLanguageDefinitionByPath.cjs");let a=require("node:fs");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);async function s(e,s){let l=new Set,u=[];for(let n of await a.default.promises.readdir(e,{withFileTypes:!0,recursive:!0})){if(!n.isFile())continue;let r=o.default.relative(e,o.default.join(n.parentPath,n.name));l.add(r);let s=await a.default.promises.readFile(o.default.join(n.parentPath,n.name),`utf8`);if(s.includes(`�`))continue;let c=i.findLanguageDefinitionByPath(n.name);c&&u.push({path:r,data:c.grammer?t.removeCommentsInSourceCode(c.grammer,s):s})}if(s.requiredSubmissionFilePaths){let e=s.requiredSubmissionFilePaths.filter(e=>!l.has(e)).toSorted();if(e.length>0)return{decisionCode:n.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`ファイルが不足しています。
1
+ "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./removeCommentsInSourceCode.cjs"),n=require("../types/problem.cjs"),r=require("./findLanguageDefinitionByPath.cjs"),i=require("../types/decisionCode.cjs");let a=require("node:fs");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);async function s(e,s){let l=new Set,u=[];for(let n of await a.default.promises.readdir(e,{withFileTypes:!0,recursive:!0})){if(!n.isFile())continue;let i=o.default.relative(e,o.default.join(n.parentPath,n.name));l.add(i);let s=await a.default.promises.readFile(o.default.join(n.parentPath,n.name),`utf8`);if(s.includes(`�`))continue;let c=r.findLanguageDefinitionByPath(n.name);c&&u.push({path:i,data:c.grammer?t.removeCommentsInSourceCode(c.grammer,s):s})}if(s.requiredSubmissionFilePaths){let e=s.requiredSubmissionFilePaths.filter(e=>!l.has(e)).toSorted();if(e.length>0)return{decisionCode:i.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`ファイルが不足しています。
2
2
  次のファイルを追加してから再度提出してください。
3
3
 
4
4
  ${e.map(e=>`- \`${e}\``).join(`
5
5
  `)}
6
- `}}let d=[];for(let e of s.forbiddenRegExpsInCode??[]){let{pattern:t,message:n}=r.normalizeCodeRule(e),i=new RegExp(t,`g`),a=u.flatMap(e=>[...e.data.matchAll(i)].map(t=>({path:e.path,match:t[0]})));a.length>0&&d.push({label:n??`禁止パターン \`${i.toString()}\``,matches:a})}for(let e of s.forbiddenTextsInCode??[]){let{pattern:t,message:n}=r.normalizeCodeRule(e),i=u.filter(e=>e.data.includes(t)).map(e=>({path:e.path,match:t}));i.length>0&&d.push({label:n??`禁止文字列 \`${t}\``,matches:i})}if(d.length>0)return{decisionCode:n.DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
6
+ `}}let d=[];for(let e of s.forbiddenRegExpsInCode??[]){let{pattern:t,message:r}=n.normalizeCodeRule(e),i=new RegExp(t,`g`),a=u.flatMap(e=>[...e.data.matchAll(i)].map(t=>({path:e.path,match:t[0]})));a.length>0&&d.push({label:r??`禁止パターン \`${i.toString()}\``,matches:a})}for(let e of s.forbiddenTextsInCode??[]){let{pattern:t,message:r}=n.normalizeCodeRule(e),i=u.filter(e=>e.data.includes(t)).map(e=>({path:e.path,match:t}));i.length>0&&d.push({label:r??`禁止文字列 \`${t}\``,matches:i})}if(d.length>0)return{decisionCode:i.DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
7
7
  ソースコードを修正してから再度提出してください。
8
8
 
9
9
  ${d.map(c).join(`
10
10
  `)}
11
- `};let f=[];for(let e of s.requiredRegExpsInCode??[]){let{pattern:t,message:n}=r.normalizeCodeRule(e),i=new RegExp(t);u.some(e=>i.test(e.data))||f.push(n??`\`${i.toString()}\``)}if(f.length>0)return{decisionCode:n.DecisionCode.REQUIRED_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に必要な文字列が含まれていません。
11
+ `};let f=[];for(let e of s.requiredRegExpsInCode??[]){let{pattern:t,message:r}=n.normalizeCodeRule(e),i=new RegExp(t);u.some(e=>i.test(e.data))||f.push(r??`\`${i.toString()}\``)}if(f.length>0)return{decisionCode:i.DecisionCode.REQUIRED_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に必要な文字列が含まれていません。
12
12
  ソースコードを修正してから再度提出してください。
13
13
 
14
14
  ${f.map(e=>`- ${e}`).join(`
@@ -1,14 +1,14 @@
1
- import{removeCommentsInSourceCode as e}from"./removeCommentsInSourceCode.js";import{DecisionCode as t}from"../types/decisionCode.js";import{normalizeCodeRule as n}from"../types/problem.js";import{findLanguageDefinitionByPath as r}from"./findLanguageDefinitionByPath.js";import i from"node:fs";import a from"node:path";async function o(o,c){let l=new Set,u=[];for(let t of await i.promises.readdir(o,{withFileTypes:!0,recursive:!0})){if(!t.isFile())continue;let n=a.relative(o,a.join(t.parentPath,t.name));l.add(n);let s=await i.promises.readFile(a.join(t.parentPath,t.name),`utf8`);if(s.includes(`�`))continue;let c=r(t.name);c&&u.push({path:n,data:c.grammer?e(c.grammer,s):s})}if(c.requiredSubmissionFilePaths){let e=c.requiredSubmissionFilePaths.filter(e=>!l.has(e)).toSorted();if(e.length>0)return{decisionCode:t.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`ファイルが不足しています。
1
+ import{removeCommentsInSourceCode as e}from"./removeCommentsInSourceCode.js";import{normalizeCodeRule as t}from"../types/problem.js";import{findLanguageDefinitionByPath as n}from"./findLanguageDefinitionByPath.js";import{DecisionCode as r}from"../types/decisionCode.js";import i from"node:fs";import a from"node:path";async function o(o,c){let l=new Set,u=[];for(let t of await i.promises.readdir(o,{withFileTypes:!0,recursive:!0})){if(!t.isFile())continue;let r=a.relative(o,a.join(t.parentPath,t.name));l.add(r);let s=await i.promises.readFile(a.join(t.parentPath,t.name),`utf8`);if(s.includes(`�`))continue;let c=n(t.name);c&&u.push({path:r,data:c.grammer?e(c.grammer,s):s})}if(c.requiredSubmissionFilePaths){let e=c.requiredSubmissionFilePaths.filter(e=>!l.has(e)).toSorted();if(e.length>0)return{decisionCode:r.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`ファイルが不足しています。
2
2
  次のファイルを追加してから再度提出してください。
3
3
 
4
4
  ${e.map(e=>`- \`${e}\``).join(`
5
5
  `)}
6
- `}}let d=[];for(let e of c.forbiddenRegExpsInCode??[]){let{pattern:t,message:r}=n(e),i=new RegExp(t,`g`),a=u.flatMap(e=>[...e.data.matchAll(i)].map(t=>({path:e.path,match:t[0]})));a.length>0&&d.push({label:r??`禁止パターン \`${i.toString()}\``,matches:a})}for(let e of c.forbiddenTextsInCode??[]){let{pattern:t,message:r}=n(e),i=u.filter(e=>e.data.includes(t)).map(e=>({path:e.path,match:t}));i.length>0&&d.push({label:r??`禁止文字列 \`${t}\``,matches:i})}if(d.length>0)return{decisionCode:t.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
6
+ `}}let d=[];for(let e of c.forbiddenRegExpsInCode??[]){let{pattern:n,message:r}=t(e),i=new RegExp(n,`g`),a=u.flatMap(e=>[...e.data.matchAll(i)].map(t=>({path:e.path,match:t[0]})));a.length>0&&d.push({label:r??`禁止パターン \`${i.toString()}\``,matches:a})}for(let e of c.forbiddenTextsInCode??[]){let{pattern:n,message:r}=t(e),i=u.filter(e=>e.data.includes(n)).map(e=>({path:e.path,match:n}));i.length>0&&d.push({label:r??`禁止文字列 \`${n}\``,matches:i})}if(d.length>0)return{decisionCode:r.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
7
7
  ソースコードを修正してから再度提出してください。
8
8
 
9
9
  ${d.map(s).join(`
10
10
  `)}
11
- `};let f=[];for(let e of c.requiredRegExpsInCode??[]){let{pattern:t,message:r}=n(e),i=new RegExp(t);u.some(e=>i.test(e.data))||f.push(r??`\`${i.toString()}\``)}if(f.length>0)return{decisionCode:t.REQUIRED_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に必要な文字列が含まれていません。
11
+ `};let f=[];for(let e of c.requiredRegExpsInCode??[]){let{pattern:n,message:r}=t(e),i=new RegExp(n);u.some(e=>i.test(e.data))||f.push(r??`\`${i.toString()}\``)}if(f.length>0)return{decisionCode:r.REQUIRED_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に必要な文字列が含まれていません。
12
12
  ソースコードを修正してから再度提出してください。
13
13
 
14
14
  ${f.map(e=>`- ${e}`).join(`
@@ -0,0 +1,2 @@
1
+ "use strict";const e=/example/;exports.EXAMPLE_TEST_CASE_ID_PATTERN=e,exports.MAX_STDOUT_LENGTH=5e4;
2
+ //# sourceMappingURL=stdioJudgeRules.cjs.map
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ /** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */
2
+ export declare const MAX_STDOUT_LENGTH = 50000;
3
+ /** The judge server shows a test case on the problem page when its id contains `example`. */
4
+ export declare const EXAMPLE_TEST_CASE_ID_PATTERN: RegExp;
@@ -0,0 +1,2 @@
1
+ const e=5e4,t=/example/;export{t as EXAMPLE_TEST_CASE_ID_PATTERN,e as MAX_STDOUT_LENGTH};
2
+ //# sourceMappingURL=stdioJudgeRules.js.map
@@ -0,0 +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"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/compareStdoutAsSpaceSeparatedTokens.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/compareExpectedOutputFiles.cjs"),i=require("./helpers/defaultStdioHarness.cjs"),a=require("./helpers/parseArgs.cjs"),o=require("./helpers/parseCsv.cjs"),s=require("./helpers/readTestCases.cjs"),c=require("./helpers/removeCommentsInSourceCode.cjs"),l=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),u=require("./helpers/runPythonInTemporaryUvProject.cjs"),d=require("./helpers/sourceCodeGrammars.cjs"),f=require("./helpers/startHttpServer.cjs"),p=require("./types/decisionCode.cjs"),m=require("./types/problem.cjs");exports.CsvParseError=o.CsvParseError,exports.DecisionCode=p.DecisionCode,exports.HARNESS_FILE_PRESETS=i.HARNESS_FILE_PRESETS,exports.MAX_COMPARED_FILE_BYTES=r.MAX_COMPARED_FILE_BYTES,exports.MAX_REPORTED_FILE_BYTES=r.MAX_REPORTED_FILE_BYTES,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.codeRuleSchema=m.codeRuleSchema,exports.compareExpectedOutputFiles=r.compareExpectedOutputFiles,exports.compareStdoutAsSpaceSeparatedTokens=e.compareStdoutAsSpaceSeparatedTokens,exports.copyPackageManagerProjectFiles=l.copyPackageManagerProjectFiles,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=i.findDefaultStdioHarnessFiles,exports.isDefaultStdioHarnessSource=i.isDefaultStdioHarnessSource,exports.judgeAgainstExpectations=r.judgeAgainstExpectations,exports.languageIdToSourceCodeGrammar=d.languageIdToSourceCodeGrammar,exports.mergeComparisonOutputFiles=r.mergeComparisonOutputFiles,exports.normalizeCodeRule=m.normalizeCodeRule,exports.parseArgs=a.parseArgs,exports.parseCsv=o.parseCsv,exports.parseCsvRecords=o.parseCsvRecords,exports.printTestCaseResult=n.printTestCaseResult,exports.problemMarkdownFrontMatterSchema=m.problemMarkdownFrontMatterSchema,exports.readTestCases=s.readTestCases,exports.removeCommentsAndStringsInSourceCode=c.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=c.removeCommentsInSourceCode,exports.runCommandInTemporaryPackageManagerProject=l.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=u.runPythonInTemporaryUvProject,exports.startHttpServer=f.startHttpServer,exports.testCaseResultSchema=t.testCaseResultSchema;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/compareStdoutAsSpaceSeparatedTokens.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/compareExpectedOutputFiles.cjs"),i=require("./helpers/defaultStdioHarness.cjs"),a=require("./helpers/parseArgs.cjs"),o=require("./helpers/parseCsv.cjs"),s=require("./helpers/readTestCases.cjs"),c=require("./helpers/removeCommentsInSourceCode.cjs"),l=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),u=require("./helpers/runPythonInTemporaryUvProject.cjs"),d=require("./helpers/sourceCodeGrammars.cjs"),f=require("./helpers/startHttpServer.cjs"),p=require("./types/problem.cjs"),m=require("./learningMaterial/validationResult.cjs"),h=require("./learningMaterial/validateMaterial.cjs"),g=require("./learningMaterial/validateContest.cjs"),_=require("./learningMaterial/validateCourse.cjs"),v=require("./learningMaterial/validateProblem.cjs"),y=require("./types/decisionCode.cjs");exports.CsvParseError=o.CsvParseError,exports.DecisionCode=y.DecisionCode,exports.HARNESS_FILE_PRESETS=i.HARNESS_FILE_PRESETS,exports.MAX_COMPARED_FILE_BYTES=r.MAX_COMPARED_FILE_BYTES,exports.MAX_REPORTED_FILE_BYTES=r.MAX_REPORTED_FILE_BYTES,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.codeRuleSchema=p.codeRuleSchema,exports.compareExpectedOutputFiles=r.compareExpectedOutputFiles,exports.compareStdoutAsSpaceSeparatedTokens=e.compareStdoutAsSpaceSeparatedTokens,exports.copyPackageManagerProjectFiles=l.copyPackageManagerProjectFiles,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=i.findDefaultStdioHarnessFiles,exports.formatValidationReport=m.formatValidationReport,exports.formatZodIssues=m.formatZodIssues,exports.isDefaultStdioHarnessSource=i.isDefaultStdioHarnessSource,exports.judgeAgainstExpectations=r.judgeAgainstExpectations,exports.languageIdToSourceCodeGrammar=d.languageIdToSourceCodeGrammar,exports.mergeComparisonOutputFiles=r.mergeComparisonOutputFiles,exports.mergeSubmissionPeriods=h.mergeSubmissionPeriods,exports.normalizeCodeRule=p.normalizeCodeRule,exports.parseArgs=a.parseArgs,exports.parseCsv=o.parseCsv,exports.parseCsvRecords=o.parseCsvRecords,exports.printTestCaseResult=n.printTestCaseResult,exports.problemMarkdownFrontMatterSchema=p.problemMarkdownFrontMatterSchema,exports.readTestCases=s.readTestCases,exports.removeCommentsAndStringsInSourceCode=c.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=c.removeCommentsInSourceCode,exports.reportDuplicateIds=m.reportDuplicateIds,exports.reportExcessChatMarkers=h.reportExcessChatMarkers,exports.reportForeignProblemCourseIds=h.reportForeignProblemCourseIds,exports.runCommandInTemporaryPackageManagerProject=l.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=u.runPythonInTemporaryUvProject,exports.startHttpServer=f.startHttpServer,exports.testCaseResultSchema=t.testCaseResultSchema,exports.validateContestFile=g.validateContestFile,exports.validateCourseDirectory=_.validateCourseDirectory,exports.validateMaterialFile=h.validateMaterialFile,exports.validateProblemDirectory=v.validateProblemDirectory,exports.validateSubmissionPeriods=h.validateSubmissionPeriods;
package/dist/index.d.ts CHANGED
@@ -10,6 +10,11 @@ export * from './helpers/runCommandInTemporaryPackageManagerProject.js';
10
10
  export * from './helpers/runPythonInTemporaryUvProject.js';
11
11
  export * from './helpers/sourceCodeGrammars.js';
12
12
  export * from './helpers/startHttpServer.js';
13
+ export * from './learningMaterial/validateContest.js';
14
+ export * from './learningMaterial/validateCourse.js';
15
+ export * from './learningMaterial/validateMaterial.js';
16
+ export * from './learningMaterial/validateProblem.js';
17
+ export * from './learningMaterial/validationResult.js';
13
18
  export * from './types/decisionCode.js';
14
19
  export * from './types/problem.js';
15
20
  export * from './types/testCaseResult.js';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{compareStdoutAsSpaceSeparatedTokens as e}from"./helpers/compareStdoutAsSpaceSeparatedTokens.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{MAX_COMPARED_FILE_BYTES as a,MAX_REPORTED_FILE_BYTES as o,compareExpectedOutputFiles as s,judgeAgainstExpectations as c,mergeComparisonOutputFiles as l}from"./helpers/compareExpectedOutputFiles.js";import{HARNESS_FILE_PRESETS as u,findDefaultStdioHarnessFiles as d,isDefaultStdioHarnessSource as f}from"./helpers/defaultStdioHarness.js";import{parseArgs as p}from"./helpers/parseArgs.js";import{CsvParseError as m,parseCsv as h,parseCsvRecords as g}from"./helpers/parseCsv.js";import{readTestCases as _}from"./helpers/readTestCases.js";import{removeCommentsAndStringsInSourceCode as v,removeCommentsInSourceCode as y}from"./helpers/removeCommentsInSourceCode.js";import{copyPackageManagerProjectFiles as b,runCommandInTemporaryPackageManagerProject as x}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as S}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as C}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as w}from"./helpers/startHttpServer.js";import{DecisionCode as T}from"./types/decisionCode.js";import{codeRuleSchema as E,normalizeCodeRule as D,problemMarkdownFrontMatterSchema as O}from"./types/problem.js";export{m as CsvParseError,T as DecisionCode,u as HARNESS_FILE_PRESETS,a as MAX_COMPARED_FILE_BYTES,o as MAX_REPORTED_FILE_BYTES,t as TEST_CASE_RESULT_PREFIX,E as codeRuleSchema,s as compareExpectedOutputFiles,e as compareStdoutAsSpaceSeparatedTokens,b as copyPackageManagerProjectFiles,r as encodeFileForTestCaseResult,d as findDefaultStdioHarnessFiles,f as isDefaultStdioHarnessSource,c as judgeAgainstExpectations,C as languageIdToSourceCodeGrammar,l as mergeComparisonOutputFiles,D as normalizeCodeRule,p as parseArgs,h as parseCsv,g as parseCsvRecords,i as printTestCaseResult,O as problemMarkdownFrontMatterSchema,_ as readTestCases,v as removeCommentsAndStringsInSourceCode,y as removeCommentsInSourceCode,x as runCommandInTemporaryPackageManagerProject,S as runPythonInTemporaryUvProject,w as startHttpServer,n as testCaseResultSchema};
1
+ import{compareStdoutAsSpaceSeparatedTokens as e}from"./helpers/compareStdoutAsSpaceSeparatedTokens.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{MAX_COMPARED_FILE_BYTES as a,MAX_REPORTED_FILE_BYTES as o,compareExpectedOutputFiles as s,judgeAgainstExpectations as c,mergeComparisonOutputFiles as l}from"./helpers/compareExpectedOutputFiles.js";import{HARNESS_FILE_PRESETS as u,findDefaultStdioHarnessFiles as d,isDefaultStdioHarnessSource as f}from"./helpers/defaultStdioHarness.js";import{parseArgs as p}from"./helpers/parseArgs.js";import{CsvParseError as m,parseCsv as h,parseCsvRecords as g}from"./helpers/parseCsv.js";import{readTestCases as _}from"./helpers/readTestCases.js";import{removeCommentsAndStringsInSourceCode as v,removeCommentsInSourceCode as y}from"./helpers/removeCommentsInSourceCode.js";import{copyPackageManagerProjectFiles as b,runCommandInTemporaryPackageManagerProject as x}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as S}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as C}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as w}from"./helpers/startHttpServer.js";import{codeRuleSchema as T,normalizeCodeRule as E,problemMarkdownFrontMatterSchema as D}from"./types/problem.js";import{formatValidationReport as O,formatZodIssues as k,reportDuplicateIds as A}from"./learningMaterial/validationResult.js";import{mergeSubmissionPeriods as j,reportExcessChatMarkers as M,reportForeignProblemCourseIds as N,validateMaterialFile as P,validateSubmissionPeriods as F}from"./learningMaterial/validateMaterial.js";import{validateContestFile as I}from"./learningMaterial/validateContest.js";import{validateCourseDirectory as L}from"./learningMaterial/validateCourse.js";import{validateProblemDirectory as R}from"./learningMaterial/validateProblem.js";import{DecisionCode as z}from"./types/decisionCode.js";export{m as CsvParseError,z as DecisionCode,u as HARNESS_FILE_PRESETS,a as MAX_COMPARED_FILE_BYTES,o as MAX_REPORTED_FILE_BYTES,t as TEST_CASE_RESULT_PREFIX,T as codeRuleSchema,s as compareExpectedOutputFiles,e as compareStdoutAsSpaceSeparatedTokens,b as copyPackageManagerProjectFiles,r as encodeFileForTestCaseResult,d as findDefaultStdioHarnessFiles,O as formatValidationReport,k as formatZodIssues,f as isDefaultStdioHarnessSource,c as judgeAgainstExpectations,C as languageIdToSourceCodeGrammar,l as mergeComparisonOutputFiles,j as mergeSubmissionPeriods,E as normalizeCodeRule,p as parseArgs,h as parseCsv,g as parseCsvRecords,i as printTestCaseResult,D as problemMarkdownFrontMatterSchema,_ as readTestCases,v as removeCommentsAndStringsInSourceCode,y as removeCommentsInSourceCode,A as reportDuplicateIds,M as reportExcessChatMarkers,N as reportForeignProblemCourseIds,x as runCommandInTemporaryPackageManagerProject,S as runPythonInTemporaryUvProject,w as startHttpServer,n as testCaseResultSchema,I as validateContestFile,L as validateCourseDirectory,P as validateMaterialFile,R as validateProblemDirectory,F as validateSubmissionPeriods};
@@ -0,0 +1,2 @@
1
+ "use strict";let e=require("yaml");const t=/^\uFEFF?(= yaml =|---)\r?\n([\s\S]*?)^(?:\1|\.\.\.)[ \t]*\r?(?:\n|(?!.))/m,n=/^\uFEFF?(?:= yaml =|---)/;function r(r){let i=t.exec(r);if(!i||i.index!==0){if(n.test(r))throw Error(`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`);return{attributes:{},body:r}}return{attributes:(0,e.parse)(i[2]??``)??{},body:r.slice(i[0].length)}}exports.parseFrontmatter=r;
2
+ //# sourceMappingURL=frontmatter.cjs.map
@@ -0,0 +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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Splits a Markdown document into YAML frontmatter attributes and the remaining body.
3
+ * Throws when the frontmatter block is present but contains invalid YAML.
4
+ */
5
+ export declare function parseFrontmatter(text: string): {
6
+ attributes: unknown;
7
+ body: string;
8
+ };
@@ -0,0 +1,2 @@
1
+ import{parse as e}from"yaml";const t=/^\uFEFF?(= yaml =|---)\r?\n([\s\S]*?)^(?:\1|\.\.\.)[ \t]*\r?(?:\n|(?!.))/m,n=/^\uFEFF?(?:= yaml =|---)/;function r(r){let i=t.exec(r);if(!i||i.index!==0){if(n.test(r))throw Error(`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`);return{attributes:{},body:r}}return{attributes:e(i[2]??``)??{},body:r.slice(i[0].length)}}export{r as parseFrontmatter};
2
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("./schemas.cjs");let t=require("node:path"),n=require("node:fs/promises");const r=new Set([`__pycache__`]),i=new Set([``,`.class`,`.exe`,`.jar`,`.out`,`.pyc`,`.png`,`.gif`]);async function a(e){try{return(await(0,n.stat)(e)).isFile()}catch{return!1}}async function o(e){try{return(await(0,n.lstat)(e)).isFile()}catch{return!1}}async function s(e){try{return(await(0,n.lstat)(e)).isDirectory()}catch{return!1}}async function c(e){try{return(await(0,n.stat)(e)).isDirectory()}catch{return!1}}async function l(e){let t=new Map;return await u(e,e,t),t}async function u(e,r,i){let a=await(0,n.readdir)(r,{withFileTypes:!0});if(!(r!==e&&a.some(e=>e.isFile()&&(e.name===`course.yaml`||e.name===`course.yml`))))for(let n of a)if(n.isFile()){let a=n.name===`problem.md`?(0,t.basename)(r):n.name.endsWith(`.problem.md`)?n.name.slice(0,-11).toLowerCase():void 0;if(a===void 0)continue;let o=i.get(a)??[];o.push((0,t.relative)(e,(0,t.join)(r,n.name))),i.set(a,o)}else n.isDirectory()&&!d(r,n.name)&&await u(e,(0,t.join)(r,n.name),i)}function d(e,n){return n.startsWith(`.`)||n===`node_modules`||n===`target`||n===`temp`&&(0,t.basename)(e)===`judge-environments`}function f(t,n){for(let[r,i]of t)e.LEARNING_MATERIAL_ID_REGEX.test(r)||n.push(`problem ID "${r}" (${i.join(`, `)}) must match ${e.LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`),i.length>1&&n.push(`problem ID "${r}" is defined more than once (${i.join(`, `)}); the judge rejects the conflict`)}async function p(e,t){return await s(e)?!0:(t.push(await c(e)?`problems directory is a symbolic link, which the judge does not traverse: ${e}`:`problems directory not found: ${e}`),!1)}async function m(e,t){if(!await p(e,t))return;let n=await l(e);return f(n,t),new Set(n.keys())}function h(e,t,n,r){let i=n===void 0?``:` under ${n}`;for(let n of new Set(e))t.has(n)||r.push(`problem "${n}" is referenced but does not exist${i}`)}async function g(e){let t=[];return await _(e,e,t),t.toSorted((e,t)=>e.path.localeCompare(t.path))}async function _(e,a,o){for(let s of await(0,n.readdir)(a,{withFileTypes:!0})){let c=(0,t.join)(a,s.name);s.isDirectory()?r.has(s.name)||await _(e,c,o):s.isFile()&&!i.has((0,t.extname)(s.name).toLowerCase())&&o.push({path:(0,t.relative)(e,c),data:await(0,n.readFile)(c,`utf8`)})}}exports.collectProblemDefinitions=l,exports.collectProblemIdsFromDirectory=m,exports.isDirectory=c,exports.isFile=a,exports.isRegularDirectory=s,exports.isRegularFile=o,exports.isUsableProblemsDirectory=p,exports.readSourceFilesRecursively=g,exports.reportDanglingProblemReferences=h,exports.reportProblemDefinitionIssues=f;
2
+ //# sourceMappingURL=fsHelpers.cjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,30 @@
1
+ export interface SourceFile {
2
+ path: string;
3
+ data: string;
4
+ }
5
+ export declare function isFile(path: string): Promise<boolean>;
6
+ /** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */
7
+ export declare function isRegularFile(path: string): Promise<boolean>;
8
+ /** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */
9
+ export declare function isRegularDirectory(path: string): Promise<boolean>;
10
+ export declare function isDirectory(path: string): Promise<boolean>;
11
+ /**
12
+ * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`
13
+ * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic
14
+ * links or a nested course (whose problems belong to that nearer course). The result maps each
15
+ * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID
16
+ * defined more than once.
17
+ */
18
+ export declare function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>>;
19
+ /**
20
+ * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern
21
+ * aborts discovery of the whole repository, and an ID defined more than once is a conflict.
22
+ */
23
+ export declare function reportProblemDefinitionIssues(definitionPathsById: ReadonlyMap<string, string[]>, errors: string[]): void;
24
+ /** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */
25
+ export declare function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean>;
26
+ /** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */
27
+ export declare function collectProblemIdsFromDirectory(problemsDirectoryPath: string, errors: string[]): Promise<Set<string> | undefined>;
28
+ export declare function reportDanglingProblemReferences(problemIds: Iterable<string>, availableProblemIds: ReadonlySet<string>, problemsDirectoryPath: string | undefined, errors: string[]): void;
29
+ /** Reads all text files under a directory, with paths relative to the given root. */
30
+ export declare function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]>;
@@ -0,0 +1,2 @@
1
+ import{LEARNING_MATERIAL_ID_REGEX as e}from"./schemas.js";import{basename as t,extname as n,join as r,relative as i}from"node:path";import{lstat as a,readFile as o,readdir as s,stat as c}from"node:fs/promises";const l=new Set([`__pycache__`]),u=new Set([``,`.class`,`.exe`,`.jar`,`.out`,`.pyc`,`.png`,`.gif`]);async function d(e){try{return(await c(e)).isFile()}catch{return!1}}async function f(e){try{return(await a(e)).isFile()}catch{return!1}}async function p(e){try{return(await a(e)).isDirectory()}catch{return!1}}async function m(e){try{return(await c(e)).isDirectory()}catch{return!1}}async function h(e){let t=new Map;return await g(e,e,t),t}async function g(e,n,a){let o=await s(n,{withFileTypes:!0});if(!(n!==e&&o.some(e=>e.isFile()&&(e.name===`course.yaml`||e.name===`course.yml`))))for(let s of o)if(s.isFile()){let o=s.name===`problem.md`?t(n):s.name.endsWith(`.problem.md`)?s.name.slice(0,-11).toLowerCase():void 0;if(o===void 0)continue;let c=a.get(o)??[];c.push(i(e,r(n,s.name))),a.set(o,c)}else s.isDirectory()&&!_(n,s.name)&&await g(e,r(n,s.name),a)}function _(e,n){return n.startsWith(`.`)||n===`node_modules`||n===`target`||n===`temp`&&t(e)===`judge-environments`}function v(t,n){for(let[r,i]of t)e.test(r)||n.push(`problem ID "${r}" (${i.join(`, `)}) must match ${e}; the judge aborts discovery on it`),i.length>1&&n.push(`problem ID "${r}" is defined more than once (${i.join(`, `)}); the judge rejects the conflict`)}async function y(e,t){return await p(e)?!0:(t.push(await m(e)?`problems directory is a symbolic link, which the judge does not traverse: ${e}`:`problems directory not found: ${e}`),!1)}async function b(e,t){if(!await y(e,t))return;let n=await h(e);return v(n,t),new Set(n.keys())}function x(e,t,n,r){let i=n===void 0?``:` under ${n}`;for(let n of new Set(e))t.has(n)||r.push(`problem "${n}" is referenced but does not exist${i}`)}async function S(e){let t=[];return await C(e,e,t),t.toSorted((e,t)=>e.path.localeCompare(t.path))}async function C(e,t,a){for(let c of await s(t,{withFileTypes:!0})){let s=r(t,c.name);c.isDirectory()?l.has(c.name)||await C(e,s,a):c.isFile()&&!u.has(n(c.name).toLowerCase())&&a.push({path:i(e,s),data:await o(s,`utf8`)})}}export{h as collectProblemDefinitions,b as collectProblemIdsFromDirectory,m as isDirectory,d as isFile,p as isRegularDirectory,f as isRegularFile,y as isUsableProblemsDirectory,S as readSourceFilesRecursively,x as reportDanglingProblemReferences,v as reportProblemDefinitionIssues};
2
+ //# sourceMappingURL=fsHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fsHelpers.js","names":[],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("../types/language.cjs"),t=require("../types/problem.cjs");let n=require("zod");const r=/^[0-9_a-z-]+$/,i=n.z.string().regex(r),a=Object.keys(e.languageIdToDefinition),o=n.z.enum(a),s=n.z.string().regex(/^\w+$/),c=n.z.strictObject({name:n.z.string().min(1),type:n.z.enum([`prompt_study`]).optional(),timeLimitMs:n.z.number().int().min(0).optional(),memoryLimitByte:n.z.number().int().min(0).optional(),requiredRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenTextsInCode:t.codeRuleSchema.array().default([]),canCreateFiles:n.z.boolean().optional(),isEditorDisabled:n.z.boolean().optional(),isAttachedFileRequired:n.z.boolean().optional(),isManualScoringRequired:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),requiredEnvironmentVariables:n.z.array(s).default([]),requiredOutputFilePaths:n.z.string().min(1).array().default([]),requiredSubmissionFilePaths:n.z.string().min(1).array().default([])}),l={availableLanguageIds:o.array().optional(),areTestCasesHidden:n.z.boolean().optional(),isProblemGradingResultHidden:n.z.boolean().optional(),isAutoFormatDisabled:n.z.boolean().optional(),isCopyAndPasteDisabled:n.z.boolean().optional(),isDebugHintDisabled:n.z.boolean().optional(),isFixHintDisabled:n.z.boolean().optional(),isDiffHintDisabled:n.z.boolean().optional(),debugHintWaitingSeconds:n.z.number().min(0).optional(),fixHintWaitingSeconds:n.z.number().min(0).optional(),diffHintWaitingSeconds:n.z.number().min(0).optional(),submissionOpenedAt:n.z.iso.datetime({offset:!0}).optional(),submissionSoftClosedAt:n.z.iso.datetime({offset:!0}).optional(),submissionHardClosedAt:n.z.iso.datetime({offset:!0}).optional(),isAutoTranslationDisabled:n.z.boolean().optional(),isModelAnswerShownAfterDeadline:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),isMaterialChatDisabled:n.z.boolean().optional()},u=n.z.strictObject({...l,name:n.z.string().min(1),description:n.z.string(),author:n.z.string().min(1).optional(),isMotivationFeatureEnabled:n.z.boolean().optional(),isPublic:n.z.boolean().optional(),lectures:n.z.array(n.z.object({id:i,name:n.z.string().min(1),description:n.z.string()})).optional()}),d=n.z.string().array().min(1).refine(e=>e.length===new Set(e).size,{error:`options must contain unique values`}),f=n.z.string().regex(/^\d+$/,{error:`must be a non-negative integer index (quoted numbers such as "0" are accepted)`}).transform(Number),p={id:i,isResubmittable:n.z.boolean().optional(),isSurvey:n.z.boolean().optional(),explanation:n.z.string().optional(),hint:n.z.string().optional()},m={...p,type:n.z.literal(`select`),options:d,answerIndex:n.z.union([n.z.number().int().nonnegative(),f,n.z.number().int().nonnegative().array(),f.array()]).optional()},h={...p,type:n.z.literal(`select_multiple`),options:d,answerIndices:n.z.union([n.z.number().int().nonnegative().array(),f.array()]).optional()},g={...p,type:n.z.literal(`text`),answerPattern:n.z.string().min(1).optional(),modelAnswer:n.z.string().optional()},_=n.z.discriminatedUnion(`type`,[n.z.strictObject({...m,question:n.z.string()}),n.z.strictObject({...h,question:n.z.string()}),n.z.strictObject({...g,question:n.z.string()})]),v=n.z.discriminatedUnion(`type`,[n.z.strictObject({...m,question:n.z.string().optional()}),n.z.strictObject({...h,question:n.z.string().optional()}),n.z.strictObject({...g,question:n.z.string().optional()})]),y=n.z.strictObject({...l,name:n.z.string().min(1),problems:n.z.object({id:i,courseId:i.optional()}).array().optional(),questions:n.z.array(v).optional(),turtleGraphicsQuestions:n.z.object({id:i}).array().optional(),isExamination:n.z.boolean().optional(),isMockExamination:n.z.boolean().optional(),isRealtimeSurvey:n.z.boolean().optional()}),b=n.z.strictObject({id:i,name:n.z.string().min(1),openedAt:n.z.iso.datetime({offset:!0}),closedAt:n.z.iso.datetime({offset:!0}),password:n.z.string().min(1).optional()}).refine(e=>new Date(e.openedAt)<new Date(e.closedAt),{error:`openedAt must be earlier than closedAt`}),x=n.z.strictObject({id:i,score:n.z.number().int().nonnegative(),courseId:i.optional(),areTestCasesHidden:l.areTestCasesHidden,isDebugHintDisabled:l.isDebugHintDisabled,isFixHintDisabled:l.isFixHintDisabled,isDiffHintDisabled:l.isDiffHintDisabled,debugHintWaitingSeconds:l.debugHintWaitingSeconds,fixHintWaitingSeconds:l.fixHintWaitingSeconds,diffHintWaitingSeconds:l.diffHintWaitingSeconds,isMaterialChatDisabled:l.isMaterialChatDisabled}),S=n.z.strictObject({...l,name:n.z.string().min(1),description:n.z.string().optional(),adminEmails:n.z.array(n.z.string().trim().email()).optional(),showsProblemsAfterClose:n.z.boolean().optional(),divisions:n.z.array(b).min(1),problems:n.z.array(x).min(1)});exports.CONTEST_MATERIAL_FILE_SUFFIX=`.contest.yaml`,exports.LEARNING_MATERIAL_ID_REGEX=r,exports.availableLanguageIds=a,exports.contestFileSchema=S,exports.courseFileSchema=u,exports.learningMaterialIdSchema=i,exports.materialFrontmatterSchema=y,exports.problemFrontmatterSchema=c,exports.questionInCodeBlockSchema=_;
2
+ //# sourceMappingURL=schemas.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.cjs","names":["z","languageIdToDefinition","codeRuleSchema"],"sources":["../../src/learningMaterial/schemas.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { languageIdToDefinition } from '../types/language.js';\nimport { codeRuleSchema } from '../types/problem.js';\n\n// Schemas in this file mirror the Exercode judge importer so that generated materials validate\n// exactly like the importer would parse them.\n\nexport const LEARNING_MATERIAL_ID_REGEX = /^[0-9_a-z-]+$/;\n\nexport const learningMaterialIdSchema = z.string().regex(LEARNING_MATERIAL_ID_REGEX);\n\nexport const availableLanguageIds = Object.keys(languageIdToDefinition);\nconst availableLanguageIdSchema = z.enum(availableLanguageIds);\n\nconst environmentVariableNameSchema = z.string().regex(/^\\w+$/);\n\n// Mirrors the judge server's strict problem frontmatter schema; keys the server removed with the\n// v1 judge (judgeEnvironmentId, generalJudgeEnvironmentConfigOverrides, testCases, isGui) are\n// rejected here as unknown keys as well.\nexport const problemFrontmatterSchema = z.strictObject({\n name: z.string().min(1),\n type: z.enum(['prompt_study']).optional(),\n timeLimitMs: z.number().int().min(0).optional(),\n memoryLimitByte: z.number().int().min(0).optional(),\n requiredRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenRegExpsInCode: codeRuleSchema.array().default([]),\n forbiddenTextsInCode: codeRuleSchema.array().default([]),\n canCreateFiles: z.boolean().optional(),\n isEditorDisabled: z.boolean().optional(),\n isAttachedFileRequired: z.boolean().optional(),\n isManualScoringRequired: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n requiredEnvironmentVariables: z.array(environmentVariableNameSchema).default([]),\n requiredOutputFilePaths: z.string().min(1).array().default([]),\n requiredSubmissionFilePaths: z.string().min(1).array().default([]),\n});\n\nexport type ProblemFrontmatter = z.infer<typeof problemFrontmatterSchema>;\n\nconst materialConfigShape = {\n availableLanguageIds: availableLanguageIdSchema.array().optional(),\n areTestCasesHidden: z.boolean().optional(),\n isProblemGradingResultHidden: z.boolean().optional(),\n isAutoFormatDisabled: z.boolean().optional(),\n isCopyAndPasteDisabled: z.boolean().optional(),\n isDebugHintDisabled: z.boolean().optional(),\n isFixHintDisabled: z.boolean().optional(),\n isDiffHintDisabled: z.boolean().optional(),\n debugHintWaitingSeconds: z.number().min(0).optional(),\n fixHintWaitingSeconds: z.number().min(0).optional(),\n diffHintWaitingSeconds: z.number().min(0).optional(),\n // z.iso.datetime is the zod v4 API (this repo pins zod 4); z.string().datetime is the v3 form.\n submissionOpenedAt: z.iso.datetime({ offset: true }).optional(),\n submissionSoftClosedAt: z.iso.datetime({ offset: true }).optional(),\n submissionHardClosedAt: z.iso.datetime({ offset: true }).optional(),\n isAutoTranslationDisabled: z.boolean().optional(),\n isModelAnswerShownAfterDeadline: z.boolean().optional(),\n isVotable: z.boolean().optional(),\n isMaterialChatDisabled: z.boolean().optional(),\n} as const;\n\nexport const courseFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string(),\n author: z.string().min(1).optional(),\n isMotivationFeatureEnabled: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n // The importer strips unknown keys of a lecture entry (plain object) while the course object is strict.\n lectures: z\n .array(\n z.object({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n description: z.string(),\n })\n )\n .optional(),\n});\n\nexport type CourseFile = z.infer<typeof courseFileSchema>;\n\nconst uniqueOptionsSchema = z\n .string()\n .array()\n .min(1)\n // zod 4 names the custom-message option `error` (`message` is the deprecated zod 3 spelling).\n .refine((options) => options.length === new Set(options).size, { error: 'options must contain unique values' });\n\n// A quoted index is coerced like the judge does, but a non-numeric string is a schema error rather\n// than a NaN that would surface as an out-of-bounds index.\nconst answerIndexStringSchema = z\n .string()\n .regex(/^\\d+$/, { error: 'must be a non-negative integer index (quoted numbers such as \"0\" are accepted)' })\n .transform(Number);\n\nconst questionBaseShape = {\n id: learningMaterialIdSchema,\n isResubmittable: z.boolean().optional(),\n isSurvey: z.boolean().optional(),\n explanation: z.string().optional(),\n hint: z.string().optional(),\n} as const;\n\nconst selectQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select'),\n options: uniqueOptionsSchema,\n answerIndex: z\n .union([\n z.number().int().nonnegative(),\n answerIndexStringSchema,\n z.number().int().nonnegative().array(),\n answerIndexStringSchema.array(),\n ])\n .optional(),\n} as const;\n\nconst selectMultipleQuestionShape = {\n ...questionBaseShape,\n type: z.literal('select_multiple'),\n options: uniqueOptionsSchema,\n answerIndices: z.union([z.number().int().nonnegative().array(), answerIndexStringSchema.array()]).optional(),\n} as const;\n\nconst textQuestionShape = {\n ...questionBaseShape,\n type: z.literal('text'),\n answerPattern: z.string().min(1).optional(),\n modelAnswer: z.string().optional(),\n} as const;\n\nexport const questionInCodeBlockSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string() }),\n z.strictObject({ ...textQuestionShape, question: z.string() }),\n]);\n\n// The judge still supports question definitions in material frontmatter where `question` is optional.\nconst questionInFrontmatterSchema = z.discriminatedUnion('type', [\n z.strictObject({ ...selectQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...selectMultipleQuestionShape, question: z.string().optional() }),\n z.strictObject({ ...textQuestionShape, question: z.string().optional() }),\n]);\n\nexport type MaterialQuestion = z.infer<typeof questionInFrontmatterSchema>;\n\nexport const materialFrontmatterSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n problems: z\n .object({ id: learningMaterialIdSchema, courseId: learningMaterialIdSchema.optional() })\n .array()\n .optional(),\n questions: z.array(questionInFrontmatterSchema).optional(),\n turtleGraphicsQuestions: z.object({ id: learningMaterialIdSchema }).array().optional(),\n isExamination: z.boolean().optional(),\n isMockExamination: z.boolean().optional(),\n isRealtimeSurvey: z.boolean().optional(),\n});\n\nexport const CONTEST_MATERIAL_FILE_SUFFIX = '.contest.yaml';\n\nconst contestDivisionSchema = z\n .strictObject({\n id: learningMaterialIdSchema,\n name: z.string().min(1),\n openedAt: z.iso.datetime({ offset: true }),\n closedAt: z.iso.datetime({ offset: true }),\n password: z.string().min(1).optional(),\n })\n .refine((division) => new Date(division.openedAt) < new Date(division.closedAt), {\n error: 'openedAt must be earlier than closedAt',\n });\n\nconst contestProblemSchema = z.strictObject({\n id: learningMaterialIdSchema,\n score: z.number().int().nonnegative(),\n courseId: learningMaterialIdSchema.optional(),\n // Per-problem overrides reuse the shared material-config fields, mirroring the judge schema.\n areTestCasesHidden: materialConfigShape.areTestCasesHidden,\n isDebugHintDisabled: materialConfigShape.isDebugHintDisabled,\n isFixHintDisabled: materialConfigShape.isFixHintDisabled,\n isDiffHintDisabled: materialConfigShape.isDiffHintDisabled,\n debugHintWaitingSeconds: materialConfigShape.debugHintWaitingSeconds,\n fixHintWaitingSeconds: materialConfigShape.fixHintWaitingSeconds,\n diffHintWaitingSeconds: materialConfigShape.diffHintWaitingSeconds,\n isMaterialChatDisabled: materialConfigShape.isMaterialChatDisabled,\n});\n\nexport const contestFileSchema = z.strictObject({\n ...materialConfigShape,\n name: z.string().min(1),\n description: z.string().optional(),\n // Exercode reads the contest administrators itself and strips the key before the judge's strict schema.\n adminEmails: z.array(z.string().trim().email()).optional(),\n showsProblemsAfterClose: z.boolean().optional(),\n divisions: z.array(contestDivisionSchema).min(1),\n problems: z.array(contestProblemSchema).min(1),\n});\n"],"mappings":"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"}