@exercode/problem-utils 4.3.5 → 4.4.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 (118) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.d.ts +2 -1
  4. package/dist/cli/checkAllProblems.js +1 -1
  5. package/dist/cli/checkAllProblems.js.map +1 -1
  6. package/dist/cli/exercodeProblem.cjs.map +1 -1
  7. package/dist/cli/exercodeProblem.js.map +1 -1
  8. package/dist/cli/runSingleHarness.cjs.map +1 -1
  9. package/dist/cli/runSingleHarness.js.map +1 -1
  10. package/dist/cli/validateLearningMaterial.cjs.map +1 -1
  11. package/dist/cli/validateLearningMaterial.js.map +1 -1
  12. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  13. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  14. package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
  15. package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
  16. package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
  17. package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
  18. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs.map +1 -1
  19. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js.map +1 -1
  20. package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
  21. package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
  22. package/dist/helpers/defaultStdioHarness.cjs.map +1 -1
  23. package/dist/helpers/defaultStdioHarness.js.map +1 -1
  24. package/dist/helpers/findEntryPointFile.cjs.map +1 -1
  25. package/dist/helpers/findEntryPointFile.js.map +1 -1
  26. package/dist/helpers/findLanguageDefinitionByPath.cjs.map +1 -1
  27. package/dist/helpers/findLanguageDefinitionByPath.js.map +1 -1
  28. package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
  29. package/dist/helpers/findModelAnswerDirs.js.map +1 -1
  30. package/dist/helpers/guiRecording.cjs.map +1 -1
  31. package/dist/helpers/guiRecording.js.map +1 -1
  32. package/dist/helpers/javaJudge.cjs.map +1 -1
  33. package/dist/helpers/javaJudge.js.map +1 -1
  34. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  35. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  36. package/dist/helpers/parseArgs.cjs.map +1 -1
  37. package/dist/helpers/parseArgs.js.map +1 -1
  38. package/dist/helpers/parseCsv.cjs.map +1 -1
  39. package/dist/helpers/parseCsv.js.map +1 -1
  40. package/dist/helpers/printDebugBanner.cjs.map +1 -1
  41. package/dist/helpers/printDebugBanner.js.map +1 -1
  42. package/dist/helpers/printTestCaseResult.cjs.map +1 -1
  43. package/dist/helpers/printTestCaseResult.js.map +1 -1
  44. package/dist/helpers/readOutputFiles.cjs.map +1 -1
  45. package/dist/helpers/readOutputFiles.js.map +1 -1
  46. package/dist/helpers/readProblemMarkdownFrontMatter.cjs.map +1 -1
  47. package/dist/helpers/readProblemMarkdownFrontMatter.js.map +1 -1
  48. package/dist/helpers/readTestCases.cjs.map +1 -1
  49. package/dist/helpers/readTestCases.js.map +1 -1
  50. package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
  51. package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
  52. package/dist/helpers/resolveCwds.cjs.map +1 -1
  53. package/dist/helpers/resolveCwds.js.map +1 -1
  54. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  55. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  56. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  57. package/dist/helpers/runHarnessProcess.js.map +1 -1
  58. package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -1
  59. package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -1
  60. package/dist/helpers/runPythonInTemporaryUvProject.cjs.map +1 -1
  61. package/dist/helpers/runPythonInTemporaryUvProject.js.map +1 -1
  62. package/dist/helpers/sourceCodeGrammars.cjs.map +1 -1
  63. package/dist/helpers/sourceCodeGrammars.js.map +1 -1
  64. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  65. package/dist/helpers/spawnWithLimits.js.map +1 -1
  66. package/dist/helpers/spawnWithTimeout.cjs.map +1 -1
  67. package/dist/helpers/spawnWithTimeout.js.map +1 -1
  68. package/dist/helpers/startHttpServer.cjs.map +1 -1
  69. package/dist/helpers/startHttpServer.js.map +1 -1
  70. package/dist/helpers/stdioJudgeRules.cjs.map +1 -1
  71. package/dist/helpers/stdioJudgeRules.js.map +1 -1
  72. package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -1
  73. package/dist/helpers/temporaryProblemDirCopy.js.map +1 -1
  74. package/dist/learningMaterial/frontmatter.cjs.map +1 -1
  75. package/dist/learningMaterial/frontmatter.js.map +1 -1
  76. package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
  77. package/dist/learningMaterial/fsHelpers.js.map +1 -1
  78. package/dist/learningMaterial/schemas.cjs +1 -1
  79. package/dist/learningMaterial/schemas.cjs.map +1 -1
  80. package/dist/learningMaterial/schemas.d.ts +42 -0
  81. package/dist/learningMaterial/schemas.js +1 -1
  82. package/dist/learningMaterial/schemas.js.map +1 -1
  83. package/dist/learningMaterial/validateContest.cjs.map +1 -1
  84. package/dist/learningMaterial/validateContest.js.map +1 -1
  85. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  86. package/dist/learningMaterial/validateCourse.js.map +1 -1
  87. package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
  88. package/dist/learningMaterial/validateMaterial.js.map +1 -1
  89. package/dist/learningMaterial/validateProblem.cjs +1 -1
  90. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  91. package/dist/learningMaterial/validateProblem.js +1 -1
  92. package/dist/learningMaterial/validateProblem.js.map +1 -1
  93. package/dist/learningMaterial/validationResult.cjs.map +1 -1
  94. package/dist/learningMaterial/validationResult.js.map +1 -1
  95. package/dist/presets/command.cjs.map +1 -1
  96. package/dist/presets/command.js.map +1 -1
  97. package/dist/presets/evaluation.cjs.map +1 -1
  98. package/dist/presets/evaluation.js.map +1 -1
  99. package/dist/presets/guiCommand.cjs.map +1 -1
  100. package/dist/presets/guiCommand.js.map +1 -1
  101. package/dist/presets/springBoot.cjs.map +1 -1
  102. package/dist/presets/springBoot.js.map +1 -1
  103. package/dist/presets/stdio.cjs.map +1 -1
  104. package/dist/presets/stdio.js.map +1 -1
  105. package/dist/presets/tomcat.cjs.map +1 -1
  106. package/dist/presets/tomcat.js.map +1 -1
  107. package/dist/types/decisionCode.cjs.map +1 -1
  108. package/dist/types/decisionCode.js.map +1 -1
  109. package/dist/types/language.cjs.map +1 -1
  110. package/dist/types/language.js.map +1 -1
  111. package/dist/types/problem.cjs +1 -1
  112. package/dist/types/problem.cjs.map +1 -1
  113. package/dist/types/problem.d.ts +1 -0
  114. package/dist/types/problem.js +1 -1
  115. package/dist/types/problem.js.map +1 -1
  116. package/dist/types/testCaseResult.cjs.map +1 -1
  117. package/dist/types/testCaseResult.js.map +1 -1
  118. package/package.json +5 -5
@@ -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("../helpers/readProblemMarkdownFrontMatter.cjs"),o=require("../types/decisionCode.cjs"),s=require("../helpers/findModelAnswerDirs.cjs"),c=require("../helpers/runHarnessProcess.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);let f=require("@willbooster/shared-lib");const p=1e3,m=new Map(Object.entries(o.DecisionCode).map(([e,t])=>[t,e]));async function h(e){let t=v(e),i=u.default.resolve(t.rootDir),o=e=>(u.default.relative(i,e)||`.`).replaceAll(u.default.sep,`/`),c=await y(i),d=c.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(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 p=[],m=[];for(let e of d){let t=await n.findDefaultStdioHarnessFiles(e);if(t.length>0){p.push(`${o(e)}: ${l.formatDefaultHarnessError(t)}`);continue}let i=await s.findModelAnswerDirs(e);if(i.length===0){p.push(`${o(e)}: no model answers found under model_answers/`);continue}if(!await x(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){p.push(`${o(e)}: failed to read the problem markdown front matter: ${(0,f.getErrorMessage)(t)}`);continue}if(!a.judgesWithoutTestCases(t)){p.push(`${o(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let c=await s.findFailingModelAnswerDirs(e);m.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...c.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of p)console.error(`✗ ${e}`);let h=u.default.resolve(process.argv[1]??``),_=0;return await(0,f.forEachConcurrently)(m,t.concurrency,async e=>{let t=`${o(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await g(e,h);n===void 0?(_++,console.info(`✓ ${t}`)):(p.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${_} passed, ${p.length} failed (${m.length} runs, ${d.length} problems)`),p.length===0?0:1}async function g(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return S(`failed to copy the problem directory to a temporary location: ${(0,f.getErrorMessage)(e)}`)}let a,o;try{a=_(e,await c.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=S(`harness execution failed: ${(0,f.getErrorMessage)(e)}`)}finally{o=await i.forciblyRemoveDirectory(n)}if(o)return a;let s=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?s:S(`${a}; ${s}`)}function _(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return S(n.failureReason);if(n.exitCode!==0)return S(`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 S(`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?S(`${m.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 v(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 y(e){let t=[];return await b(e,t),t.toSorted()}async function b(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);else for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await b(u.default.join(e,r.name),t)}async function x(e){try{return await d.default.stat(e),!0}catch{return!1}}function S(e){return e.length<=p?e:`${e.slice(0,p)}...`}exports.checkAllProblems=h;
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("../learningMaterial/schemas.cjs"),o=require("../helpers/readProblemMarkdownFrontMatter.cjs"),s=require("../types/decisionCode.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("../helpers/runHarnessProcess.cjs"),u=require("./runSingleHarness.cjs");let d=require("node:path");d=e.__toESM(d,1);let f=require("node:fs/promises");f=e.__toESM(f,1);let p=require("@willbooster/shared-lib");const m=1e3,h=new Map(Object.entries(s.DecisionCode).map(([e,t])=>[t,e]));async function g(e){let t=x(e),n=d.default.resolve(t.rootDir),r=e=>(d.default.relative(n,e)||`.`).replaceAll(d.default.sep,`/`),i=await S(n),a=i.filter(e=>{let n=r(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(a.length===0)return console.error(i.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${n}.`:`All ${i.length} problem directories under ${n} were excluded by --only/--skip.`),1;let o=[],s=[],c=0;for(let e of a){let t=await _(e);t.type===`agentChallenge`?c++:t.type===`failure`?o.push(`${r(e)}: ${t.message}`):s.push(...t.runs)}for(let e of o)console.error(`✗ ${e}`);let l=d.default.resolve(process.argv[1]??``),u=0;return await(0,p.forEachConcurrently)(s,t.concurrency,async e=>{let t=`${r(e.problemDir)} ${d.default.relative(e.problemDir,e.answerDir).replaceAll(d.default.sep,`/`)}`,n=await y(e,l);n===void 0?(u++,console.info(`✓ ${t}`)):(o.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${u} passed, ${o.length} failed (${s.length} runs, ${a.length} problems${c>0?`, agent challenges skipped: ${c}`:``})`),o.length===0?0:1}async function _(e){let t=await o.readProblemMarkdownFrontMatter(e).then(e=>({frontMatter:e}),e=>({error:e}));if(`frontMatter`in t&&t.frontMatter.agentChallenge!==void 0)return v(e,t.frontMatter.agentChallenge);let i=await n.findDefaultStdioHarnessFiles(e);if(i.length>0)return{type:`failure`,message:u.formatDefaultHarnessError(i)};let a=await c.findModelAnswerDirs(e);if(a.length===0)return{type:`failure`,message:`no model answers found under model_answers/`};if(!await w(d.default.join(e,`judge.ts`))&&(await r.readTestCases(d.default.join(e,`test_cases`))).length===0){if(`error`in t)return{type:`failure`,message:`failed to read the problem markdown front matter: ${(0,p.getErrorMessage)(t.error)}`};if(!o.judgesWithoutTestCases(t.frontMatter))return{type:`failure`,message:u.MISSING_TEST_CASES_ERROR}}let s=await c.findFailingModelAnswerDirs(e);return{type:`runs`,runs:[...a.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...s.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`}))]}}async function v(e,t){let n=a.agentChallengeSchema.safeParse(t);return n.success?(await f.default.readdir(d.default.join(e,`model_answers`,`explanation`),{recursive:!0,withFileTypes:!0}).catch(()=>[])).some(e=>e.isFile()&&e.name.endsWith(`.md`))?{type:`agentChallenge`}:{type:`failure`,message:`no explanation found under model_answers/explanation/`}:{type:`failure`,message:`invalid agentChallenge (see validate-problem): ${n.error.issues.map(e=>`${[`agentChallenge`,...e.path].join(`.`)}: ${e.message}`).join(`; `)}`}}async function y(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return T(`failed to copy the problem directory to a temporary location: ${(0,p.getErrorMessage)(e)}`)}let a,o;try{a=b(e,await l.runHarnessProcess([`run`,t,`judge`,d.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){a=T(`harness execution failed: ${(0,p.getErrorMessage)(e)}`)}finally{o=await i.forciblyRemoveDirectory(n)}if(o)return a;let s=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?s:T(`${a}; ${s}`)}function b(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return T(n.failureReason);if(n.exitCode!==0)return T(`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)),o=[];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 T(`malformed test case result line: ${e}`);o.push(n.data)}if(o.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=o.find(e=>e.decisionCode!==s.DecisionCode.ACCEPTED);return e?T(`${h.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return o.every(e=>e.decisionCode===s.DecisionCode.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 f.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`))))t.push(e);else for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await C(d.default.join(e,r.name),t)}async function w(e){try{return await f.default.stat(e),!0}catch{return!1}}function T(e){return e.length<=m?e:`${e.slice(0,m)}...`}exports.checkAllProblems=g;
2
2
  //# sourceMappingURL=checkAllProblems.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","forEachConcurrently","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","fs","require_runtime","require_testCaseResult","require_defaultStdioHarness","require_readTestCases","require_temporaryProblemDirCopy","require_readProblemMarkdownFrontMatter","require_decisionCode","require_findModelAnswerDirs","require_runHarnessProcess","require_runSingleHarness","node_path","node_fs_promises","_willbooster_shared_lib","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","defaultHarnessFileNames","modelAnswerDirs","fileExists","frontMatter","error","failingModelAnswerDirs","failure","cliEntryPath","passedCount","label","run","failureDetail","executeCheckRun","tempRoot","copiedProblemDir","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","removalFailureDetail","result","stderr","resultLines","stdout","line","testCaseResults","parsedResult","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n for (const problemDir of problemDirs) {\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n failures.push(`${toRelative(problemDir)}: ${formatDefaultHarnessError(defaultHarnessFileNames)}`);\n continue;\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n failures.push(`${toRelative(problemDir)}: no model answers found under model_answers/`);\n continue;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n let frontMatter;\n try {\n frontMatter = await readProblemMarkdownFrontMatter(problemDir);\n } catch (error) {\n failures.push(\n `${toRelative(problemDir)}: failed to read the problem markdown front matter: ${getErrorMessage(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","forEachConcurrently","readProblemMarkdownFrontMatter","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","agentChallengeSchema","fs","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","require_runtime","require_testCaseResult","require_defaultStdioHarness","require_readTestCases","require_temporaryProblemDirCopy","require_schemas","require_readProblemMarkdownFrontMatter","require_decisionCode","require_findModelAnswerDirs","require_runHarnessProcess","require_runSingleHarness","node_path","node_fs_promises","_willbooster_shared_lib","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","agentChallengeCount","plan","planProblemCheck","failure","cliEntryPath","passedCount","label","run","failureDetail","executeCheckRun","frontMatterResult","planAgentChallengeCheck","defaultHarnessFileNames","modelAnswerDirs","fileExists","failingModelAnswerDirs","parsed","agentChallenge","dirent","issue","tempRoot","copiedProblemDir","error","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","removalFailureDetail","result","stderr","resultLines","stdout","line","testCaseResults","parsedResult","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { agentChallengeSchema } from '../learningMaterial/schemas.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Agent challenges have no harness, so\n * they are not judged but fail when their `agentChallenge` is invalid or their explanation is missing.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n let agentChallengeCount = 0;\n for (const problemDir of problemDirs) {\n const plan = await planProblemCheck(problemDir);\n if (plan.type === 'agentChallenge') {\n agentChallengeCount++;\n } else if (plan.type === 'failure') {\n failures.push(`${toRelative(problemDir)}: ${plan.message}`);\n } else {\n runs.push(...plan.runs);\n }\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems${agentChallengeCount > 0 ? `, agent challenges skipped: ${agentChallengeCount}` : ''})`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\ntype ProblemCheckPlan =\n { type: 'agentChallenge' } | { type: 'failure'; message: string } | { type: 'runs'; runs: CheckRun[] };\n\nasync function planProblemCheck(problemDir: string): Promise<ProblemCheckPlan> {\n // As before agent challenges existed, a front matter that fails to parse fails the problem only\n // when the test-case check below needs the front matter.\n const frontMatterResult = await readProblemMarkdownFrontMatter(problemDir).then(\n (frontMatter) => ({ frontMatter }),\n (error: unknown) => ({ error })\n );\n if ('frontMatter' in frontMatterResult && frontMatterResult.frontMatter.agentChallenge !== undefined) {\n return planAgentChallengeCheck(problemDir, frontMatterResult.frontMatter.agentChallenge);\n }\n\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n return { type: 'failure', message: formatDefaultHarnessError(defaultHarnessFileNames) };\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n return { type: 'failure', message: 'no model answers found under model_answers/' };\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n if ('error' in frontMatterResult) {\n return {\n type: 'failure',\n message: `failed to read the problem markdown front matter: ${getErrorMessage(frontMatterResult.error)}`,\n };\n }\n if (!judgesWithoutTestCases(frontMatterResult.frontMatter)) {\n return { type: 'failure', message: MISSING_TEST_CASES_ERROR };\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n return {\n type: 'runs',\n runs: [\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' })),\n ],\n };\n}\n\n/** Skips a valid agent challenge and fails an invalid one instead of judging it as a stdio problem. */\nasync function planAgentChallengeCheck(problemDir: string, agentChallenge: unknown): Promise<ProblemCheckPlan> {\n const parsed = agentChallengeSchema.safeParse(agentChallenge);\n if (parsed.success) {\n const explanationDirents = await fs\n .readdir(path.join(problemDir, 'model_answers', 'explanation'), { recursive: true, withFileTypes: true })\n .catch(() => []);\n return explanationDirents.some((dirent) => dirent.isFile() && dirent.name.endsWith('.md'))\n ? { type: 'agentChallenge' }\n : { type: 'failure', message: 'no explanation found under model_answers/explanation/' };\n }\n const issues = parsed.error.issues.map((issue) => `${['agentChallenge', ...issue.path].join('.')}: ${issue.message}`);\n return { type: 'failure', message: `invalid agentChallenge (see validate-problem): ${issues.join('; ')}` };\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Judge all model answers of all problems (directories containing `problem.md` or
3
3
  * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and
4
- * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.
4
+ * `model_answers.fails/*` must fail at least one test case. Agent challenges have no harness, so
5
+ * they are not judged but fail when their `agentChallenge` is invalid or their explanation is missing.
5
6
  */
6
7
  export declare function checkAllProblems(args: readonly string[]): Promise<number>;
@@ -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{judgesWithoutTestCases as o,readProblemMarkdownFrontMatter as s}from"../helpers/readProblemMarkdownFrontMatter.js";import{DecisionCode as c}from"../types/decisionCode.js";import{findFailingModelAnswerDirs as l,findModelAnswerDirs as u}from"../helpers/findModelAnswerDirs.js";import{runHarnessProcess as d}from"../helpers/runHarnessProcess.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";import{forEachConcurrently as g,getErrorMessage as _}from"@willbooster/shared-lib";const v=1e3,y=new Map(Object.entries(c).map(([e,t])=>[t,e]));async function b(e){let t=C(e),i=m.resolve(t.rootDir),a=e=>(m.relative(i,e)||`.`).replaceAll(m.sep,`/`),c=await w(i),d=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(d.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=[],v=[];for(let e of d){let t=await n(e);if(t.length>0){h.push(`${a(e)}: ${p(t)}`);continue}let i=await u(e);if(i.length===0){h.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await E(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)}`);continue}if(!o(t)){h.push(`${a(e)}: ${f}`);continue}}let c=await l(e);v.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 y=m.resolve(process.argv[1]??``),b=0;return await g(v,t.concurrency,async e=>{let t=`${a(e.problemDir)} ${m.relative(e.problemDir,e.answerDir).replaceAll(m.sep,`/`)}`,n=await x(e,y);n===void 0?(b++,console.info(`✓ ${t}`)):(h.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${b} passed, ${h.length} failed (${v.length} runs, ${d.length} problems)`),h.length===0?0:1}async function x(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return D(`failed to copy the problem directory to a temporary location: ${_(e)}`)}let o,s;try{o=S(e,await d([`run`,t,`judge`,m.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=D(`harness execution failed: ${_(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:D(`${o}; ${c}`)}function S(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return D(r.failureReason);if(r.exitCode!==0)return D(`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 D(`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?D(`${y.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 C(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 w(e){let t=[];return await T(e,t),t.toSorted()}async function T(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);else for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await T(m.join(e,r.name),t)}async function E(e){try{return await h.stat(e),!0}catch{return!1}}function D(e){return e.length<=v?e:`${e.slice(0,v)}...`}export{b 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{agentChallengeSchema as o}from"../learningMaterial/schemas.js";import{judgesWithoutTestCases as s,readProblemMarkdownFrontMatter as c}from"../helpers/readProblemMarkdownFrontMatter.js";import{DecisionCode as l}from"../types/decisionCode.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{runHarnessProcess as f}from"../helpers/runHarnessProcess.js";import{MISSING_TEST_CASES_ERROR as p,formatDefaultHarnessError as m}from"./runSingleHarness.js";import h from"node:path";import g from"node:fs/promises";import{forEachConcurrently as _,getErrorMessage as v}from"@willbooster/shared-lib";const y=1e3,b=new Map(Object.entries(l).map(([e,t])=>[t,e]));async function x(e){let t=E(e),n=h.resolve(t.rootDir),r=e=>(h.relative(n,e)||`.`).replaceAll(h.sep,`/`),i=await D(n),a=i.filter(e=>{let n=r(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(a.length===0)return console.error(i.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${n}.`:`All ${i.length} problem directories under ${n} were excluded by --only/--skip.`),1;let o=[],s=[],c=0;for(let e of a){let t=await S(e);t.type===`agentChallenge`?c++:t.type===`failure`?o.push(`${r(e)}: ${t.message}`):s.push(...t.runs)}for(let e of o)console.error(`✗ ${e}`);let l=h.resolve(process.argv[1]??``),u=0;return await _(s,t.concurrency,async e=>{let t=`${r(e.problemDir)} ${h.relative(e.problemDir,e.answerDir).replaceAll(h.sep,`/`)}`,n=await w(e,l);n===void 0?(u++,console.info(`✓ ${t}`)):(o.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${u} passed, ${o.length} failed (${s.length} runs, ${a.length} problems${c>0?`, agent challenges skipped: ${c}`:``})`),o.length===0?0:1}async function S(e){let t=await c(e).then(e=>({frontMatter:e}),e=>({error:e}));if(`frontMatter`in t&&t.frontMatter.agentChallenge!==void 0)return C(e,t.frontMatter.agentChallenge);let i=await n(e);if(i.length>0)return{type:`failure`,message:m(i)};let a=await d(e);if(a.length===0)return{type:`failure`,message:`no model answers found under model_answers/`};if(!await k(h.join(e,`judge.ts`))&&(await r(h.join(e,`test_cases`))).length===0){if(`error`in t)return{type:`failure`,message:`failed to read the problem markdown front matter: ${v(t.error)}`};if(!s(t.frontMatter))return{type:`failure`,message:p}}let o=await u(e);return{type:`runs`,runs:[...a.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...o.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`}))]}}async function C(e,t){let n=o.safeParse(t);return n.success?(await g.readdir(h.join(e,`model_answers`,`explanation`),{recursive:!0,withFileTypes:!0}).catch(()=>[])).some(e=>e.isFile()&&e.name.endsWith(`.md`))?{type:`agentChallenge`}:{type:`failure`,message:`no explanation found under model_answers/explanation/`}:{type:`failure`,message:`invalid agentChallenge (see validate-problem): ${n.error.issues.map(e=>`${[`agentChallenge`,...e.path].join(`.`)}: ${e.message}`).join(`; `)}`}}async function w(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return A(`failed to copy the problem directory to a temporary location: ${v(e)}`)}let o,s;try{o=T(e,await f([`run`,t,`judge`,h.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=A(`harness execution failed: ${v(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:A(`${o}; ${c}`)}function T(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return A(r.failureReason);if(r.exitCode!==0)return A(`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 A(`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!==l.ACCEPTED);return e?A(`${b.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===l.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function E(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 D(e){let t=[];return await O(e,t),t.toSorted()}async function O(e,t){let n=await g.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`))))t.push(e);else for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await O(h.join(e,r.name),t)}async function k(e){try{return await g.stat(e),!0}catch{return!1}}function A(e){return e.length<=y?e:`${e.slice(0,y)}...`}export{x as checkAllProblems};
2
2
  //# sourceMappingURL=checkAllProblems.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkAllProblems.js","names":["path","fs","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","DecisionCode","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","formatDefaultHarnessError","modelAnswerDirs","findModelAnswerDirs","fileExists","readTestCases","frontMatter","readProblemMarkdownFrontMatter","error","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","failingModelAnswerDirs","findFailingModelAnswerDirs","failure","cliEntryPath","passedCount","forEachConcurrently","label","run","failureDetail","executeCheckRun","tempRoot","copiedProblemDir","copyProblemDirToTemporaryRoot","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","runHarnessProcess","forciblyRemoveDirectory","removalFailureDetail","result","stderr","resultLines","stdout","line","TEST_CASE_RESULT_PREFIX","testCaseResults","parsedResult","testCaseResultSchema","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Returns the process exit code.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n for (const problemDir of problemDirs) {\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n failures.push(`${toRelative(problemDir)}: ${formatDefaultHarnessError(defaultHarnessFileNames)}`);\n continue;\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n failures.push(`${toRelative(problemDir)}: no model answers found under model_answers/`);\n continue;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n let frontMatter;\n try {\n frontMatter = await readProblemMarkdownFrontMatter(problemDir);\n } catch (error) {\n failures.push(\n `${toRelative(problemDir)}: failed to read the problem markdown front matter: ${getErrorMessage(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"checkAllProblems.js","names":["path","fs","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","DecisionCode","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","agentChallengeCount","plan","planProblemCheck","failure","cliEntryPath","passedCount","forEachConcurrently","label","run","failureDetail","executeCheckRun","frontMatterResult","readProblemMarkdownFrontMatter","planAgentChallengeCheck","defaultHarnessFileNames","findDefaultStdioHarnessFiles","formatDefaultHarnessError","modelAnswerDirs","findModelAnswerDirs","fileExists","readTestCases","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","failingModelAnswerDirs","findFailingModelAnswerDirs","parsed","agentChallengeSchema","agentChallenge","dirent","issue","tempRoot","copiedProblemDir","copyProblemDirToTemporaryRoot","error","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","runHarnessProcess","forciblyRemoveDirectory","removalFailureDetail","result","stderr","resultLines","stdout","line","TEST_CASE_RESULT_PREFIX","testCaseResults","parsedResult","testCaseResultSchema","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { agentChallengeSchema } from '../learningMaterial/schemas.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { formatDefaultHarnessError, MISSING_TEST_CASES_ERROR } from './runSingleHarness.js';\n\nconst RUN_TIMEOUT_MS = 600_000;\nconst MAX_RUN_OUTPUT_BYTES = 64 * 1024 * 1024;\nconst MAX_FAILURE_DETAIL_LENGTH = 1000;\n\n// DecisionCode is a plain const object (not a TypeScript enum), so it has no reverse mapping.\nconst decisionCodeNames = new Map<number, string>(Object.entries(DecisionCode).map(([name, code]) => [code, name]));\n\ninterface CheckOptions {\n rootDir: string;\n concurrency: number;\n only: string[];\n skip: string[];\n}\n\ninterface CheckRun {\n problemDir: string;\n answerDir: string;\n /** Whether all test cases must be accepted (`model_answers`) or at least one must fail (`model_answers.fails`). */\n expectation: 'accepted' | 'rejected';\n}\n\n/**\n * Judge all model answers of all problems (directories containing `problem.md` or\n * `<id>.problem.md`) under a root directory: `model_answers/*` must be fully accepted and\n * `model_answers.fails/*` must fail at least one test case. Agent challenges have no harness, so\n * they are not judged but fail when their `agentChallenge` is invalid or their explanation is missing.\n */\nexport async function checkAllProblems(args: readonly string[]): Promise<number> {\n const options = parseCheckArgs(args);\n const rootDir = path.resolve(options.rootDir);\n // Normalize to forward slashes so --only / --skip substrings like courses/foo match on Windows.\n const toRelative = (dir: string): string => (path.relative(rootDir, dir) || '.').replaceAll(path.sep, '/');\n\n const allProblemDirs = await findProblemDirs(rootDir);\n const problemDirs = allProblemDirs.filter((problemDir) => {\n const relativeDir = toRelative(problemDir);\n if (options.only.length > 0 && !options.only.some((substring) => relativeDir.includes(substring))) return false;\n return !options.skip.some((substring) => relativeDir.includes(substring));\n });\n if (problemDirs.length === 0) {\n console.error(\n allProblemDirs.length === 0\n ? `No problem directories (containing problem.md or <id>.problem.md) found under ${rootDir}.`\n : `All ${allProblemDirs.length} problem directories under ${rootDir} were excluded by --only/--skip.`\n );\n return 1;\n }\n\n const failures: string[] = [];\n const runs: CheckRun[] = [];\n let agentChallengeCount = 0;\n for (const problemDir of problemDirs) {\n const plan = await planProblemCheck(problemDir);\n if (plan.type === 'agentChallenge') {\n agentChallengeCount++;\n } else if (plan.type === 'failure') {\n failures.push(`${toRelative(problemDir)}: ${plan.message}`);\n } else {\n runs.push(...plan.runs);\n }\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems${agentChallengeCount > 0 ? `, agent challenges skipped: ${agentChallengeCount}` : ''})`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\ntype ProblemCheckPlan =\n { type: 'agentChallenge' } | { type: 'failure'; message: string } | { type: 'runs'; runs: CheckRun[] };\n\nasync function planProblemCheck(problemDir: string): Promise<ProblemCheckPlan> {\n // As before agent challenges existed, a front matter that fails to parse fails the problem only\n // when the test-case check below needs the front matter.\n const frontMatterResult = await readProblemMarkdownFrontMatter(problemDir).then(\n (frontMatter) => ({ frontMatter }),\n (error: unknown) => ({ error })\n );\n if ('frontMatter' in frontMatterResult && frontMatterResult.frontMatter.agentChallenge !== undefined) {\n return planAgentChallengeCheck(problemDir, frontMatterResult.frontMatter.agentChallenge);\n }\n\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n return { type: 'failure', message: formatDefaultHarnessError(defaultHarnessFileNames) };\n }\n\n const modelAnswerDirs = await findModelAnswerDirs(problemDir);\n if (modelAnswerDirs.length === 0) {\n return { type: 'failure', message: 'no model answers found under model_answers/' };\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (!(await fileExists(path.join(problemDir, 'judge.ts')))) {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0) {\n if ('error' in frontMatterResult) {\n return {\n type: 'failure',\n message: `failed to read the problem markdown front matter: ${getErrorMessage(frontMatterResult.error)}`,\n };\n }\n if (!judgesWithoutTestCases(frontMatterResult.frontMatter)) {\n return { type: 'failure', message: MISSING_TEST_CASES_ERROR };\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n return {\n type: 'runs',\n runs: [\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' })),\n ],\n };\n}\n\n/** Skips a valid agent challenge and fails an invalid one instead of judging it as a stdio problem. */\nasync function planAgentChallengeCheck(problemDir: string, agentChallenge: unknown): Promise<ProblemCheckPlan> {\n const parsed = agentChallengeSchema.safeParse(agentChallenge);\n if (parsed.success) {\n const explanationDirents = await fs\n .readdir(path.join(problemDir, 'model_answers', 'explanation'), { recursive: true, withFileTypes: true })\n .catch(() => []);\n return explanationDirents.some((dirent) => dirent.isFile() && dirent.name.endsWith('.md'))\n ? { type: 'agentChallenge' }\n : { type: 'failure', message: 'no explanation found under model_answers/explanation/' };\n }\n const issues = parsed.error.issues.map((issue) => `${['agentChallenge', ...issue.path].join('.')}: ${issue.message}`);\n return { type: 'failure', message: `invalid agentChallenge (see validate-problem): ${issues.join('; ')}` };\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"exercodeProblem.cjs","names":["runSingleHarness","validateLearningMaterial","checkAllProblems","VALIDATE_SUBCOMMANDS","require_runSingleHarness","require_checkAllProblems","require_validateLearningMaterial","usage","main","args","isValidateSubcommand","error","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, validateLearningMaterial, type ValidateSubcommand } 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":"uIAIMO,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAEtC,QAAQ,KAAKF,CAAK,OAGpB,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBS,EAAK,EAAE,CACnD,MAAWC,EAAqBD,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMR,EAAAA,yBAAyBQ,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBO,CAAI,CAElD,OAASE,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASD,EAAqB,EAAoD,CAChF,OAAQP,EAAAA,qBAA2C,SAASS,GAAO,EAAE,CACvE"}
1
+ {"version":3,"file":"exercodeProblem.cjs","names":["runSingleHarness","validateLearningMaterial","checkAllProblems","VALIDATE_SUBCOMMANDS","require_runSingleHarness","require_checkAllProblems","require_validateLearningMaterial","usage","main","args","isValidateSubcommand","error","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, validateLearningMaterial, type ValidateSubcommand } 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":"A,uIAIMO,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAEtC,QAAQ,KAAKF,CAAK,OAGpB,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBS,EAAK,EAAE,CACnD,MAAWC,EAAqBD,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMR,EAAAA,yBAAyBQ,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBO,CAAI,CAElD,OAASE,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASD,EAAqB,EAAoD,CAChF,OAAQP,EAAAA,qBAA2C,SAASS,GAAO,EAAE,CACvE"}
@@ -1 +1 @@
1
- {"version":3,"file":"exercodeProblem.js","names":["usage","main","args","runSingleHarness","isValidateSubcommand","validateLearningMaterial","checkAllProblems","error","VALIDATE_SUBCOMMANDS","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, validateLearningMaterial, type ValidateSubcommand } 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,MAAMA,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAEtC,QAAQ,KAAKF,CAAK,OAGpB,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMG,EAAiBD,EAAK,EAAE,CACnD,MAAWE,EAAqBF,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMG,EAAyBH,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMI,EAAiBJ,CAAI,CAElD,OAASK,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASH,EAAqB,EAAoD,CAChF,OAAQI,EAA2C,SAASC,GAAO,EAAE,CACvE"}
1
+ {"version":3,"file":"exercodeProblem.js","names":["usage","main","args","runSingleHarness","isValidateSubcommand","validateLearningMaterial","checkAllProblems","error","VALIDATE_SUBCOMMANDS","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, validateLearningMaterial, type ValidateSubcommand } 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":"A,qNAIA,MAAMA,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAEtC,QAAQ,KAAKF,CAAK,OAGpB,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMG,EAAiBD,EAAK,EAAE,CACnD,MAAWE,EAAqBF,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMG,EAAyBH,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMI,EAAiBJ,CAAI,CAElD,OAASK,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASH,EAAqB,EAAoD,CAChF,OAAQI,EAA2C,SAASC,GAAO,EAAE,CACvE"}
@@ -1 +1 @@
1
- {"version":3,"file":"runSingleHarness.cjs","names":["findDefaultStdioHarnessFiles","fs","path","child_process","readTestCases","judgesWithoutTestCases","readProblemMarkdownFrontMatter","stdioJudgePreset","stdioDebugPreset","require_runtime","require_defaultStdioHarness","require_readTestCases","require_readProblemMarkdownFrontMatter","require_stdio","node_fs","node_path","node_child_process","MISSING_TEST_CASES_ERROR","runSingleHarness","problemDir","defaultHarnessFileNames","formatDefaultHarnessError","harnessFileName","kind","spawnResult","fileNames"],"sources":["../../src/cli/runSingleHarness.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { stdioDebugPreset, stdioJudgePreset } from '../presets/stdio.js';\n\nexport const MISSING_TEST_CASES_ERROR =\n 'a standard stdio problem (without judge.ts) needs at least one test case under test_cases/, static-analysis rules (e.g. requiredRegExpsInCode), or isManualScoringRequired';\n\n/**\n * Run the judge or debug harness of the problem in the current working directory: a custom\n * `judge.ts` / `debug.ts` when present, or the default stdio preset otherwise (mirroring the\n * Exercode server, which applies the stdio presets to problems without these files).\n */\nexport async function runSingleHarness(kind: 'judge' | 'debug'): Promise<number> {\n const problemDir = process.cwd();\n\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n console.error(formatDefaultHarnessError(defaultHarnessFileNames));\n return 1;\n }\n\n const harnessFileName = kind === 'judge' ? 'judge.ts' : 'debug.ts';\n if (fs.existsSync(path.join(problemDir, harnessFileName))) {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const spawnResult = child_process.spawnSync(process.execPath, ['run', harnessFileName, ...process.argv.slice(2)], {\n cwd: problemDir,\n stdio: 'inherit',\n });\n if (spawnResult.error) {\n console.error(`failed to run ${harnessFileName}: ${spawnResult.error.message}`);\n return 1;\n }\n return spawnResult.status ?? 1;\n }\n\n // A custom judge.ts marks the problem as non-standard, so the stdio debug preset would judge the\n // answer against a contract the problem does not have (mirroring the server, which reports debug\n // as unsupported in this case).\n if (kind === 'debug' && fs.existsSync(path.join(problemDir, 'judge.ts'))) {\n console.error('This problem has a custom judge.ts but no debug.ts; add a debug.ts to support debugging.');\n return 1;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (kind === 'judge') {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0 && !judgesWithoutTestCases(await readProblemMarkdownFrontMatter(problemDir))) {\n console.error(MISSING_TEST_CASES_ERROR);\n return 1;\n }\n }\n\n // The presets read the answer directory and params from `process.argv`, which holds them at the\n // same positions as when a harness script is invoked directly.\n await (kind === 'judge' ? stdioJudgePreset(problemDir) : stdioDebugPreset(problemDir));\n return 0;\n}\n\nexport function formatDefaultHarnessError(defaultHarnessFileNames: readonly string[]): string {\n const fileNames = defaultHarnessFileNames.join(' and ');\n return `${fileNames} must not be committed because the content is identical to the default stdio harness.\nA problem without judge.ts is automatically judged with stdioJudgePreset and debugged with stdioDebugPreset,\nso delete ${fileNames} and run the problem with \\`bun x exercode-problem judge <answerDir>\\` or \\`bun x exercode-problem debug <answerDir>\\`.\nIf the file intentionally demonstrates the default harness, add an explanatory comment to mark it as custom.`;\n}\n"],"mappings":"ucASA,MAAaiB,EACX,6KAOF,eAAsBC,EAAiB,EAA0C,CAC/E,IAAMC,EAAa,QAAQ,IAAI,EAEzBC,EAA0B,MAAMpB,EAAAA,6BAA6BmB,CAAU,EAC7E,GAAIC,EAAwB,OAAS,EAEnC,OADA,QAAQ,MAAMC,EAA0BD,CAAuB,CAAC,EACzD,EAGT,IAAME,EAAkBC,IAAS,QAAU,WAAa,WACxD,GAAItB,EAAAA,QAAG,WAAWC,EAAAA,QAAK,KAAKiB,EAAYG,CAAe,CAAC,EAAG,CAEzD,IAAME,EAAcrB,EAAAA,QAAc,UAAU,QAAQ,SAAU,CAAC,MAAOmB,EAAiB,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAG,CAChH,IAAKH,EACL,MAAO,SACT,CAAC,EAKD,OAJIK,EAAY,OACd,QAAQ,MAAM,iBAAiBF,EAAgB,IAAIE,EAAY,MAAM,SAAS,EACvE,GAEFA,EAAY,QAAU,CAC/B,CAwBA,OAnBID,IAAS,SAAWtB,EAAAA,QAAG,WAAWC,EAAAA,QAAK,KAAKiB,EAAY,UAAU,CAAC,GACrE,QAAQ,MAAM,0FAA0F,EACjG,GAMLI,IAAS,UAEP,MADoBnB,EAAAA,cAAcF,EAAAA,QAAK,KAAKiB,EAAY,YAAY,CAAC,EAAA,CAC3D,SAAW,GAAK,CAACd,EAAAA,uBAAuB,MAAMC,EAAAA,+BAA+Ba,CAAU,CAAC,GACpG,QAAQ,MAAMF,CAAwB,EAC/B,IAMX,MAAOM,IAAS,QAAUhB,EAAAA,iBAAiBY,CAAU,EAAIX,EAAAA,iBAAiBW,CAAU,GAC7E,EACT,CAEA,SAAgBE,EAA0B,EAAoD,CAC5F,IAAMI,EAAYL,EAAwB,KAAK,OAAO,EACtD,MAAO,GAAGK,EAAU;;YAEVA,EAAU;6GAEtB"}
1
+ {"version":3,"file":"runSingleHarness.cjs","names":["findDefaultStdioHarnessFiles","fs","path","child_process","readTestCases","judgesWithoutTestCases","readProblemMarkdownFrontMatter","stdioJudgePreset","stdioDebugPreset","require_runtime","require_defaultStdioHarness","require_readTestCases","require_readProblemMarkdownFrontMatter","require_stdio","node_fs","node_path","node_child_process","MISSING_TEST_CASES_ERROR","runSingleHarness","problemDir","defaultHarnessFileNames","formatDefaultHarnessError","harnessFileName","kind","spawnResult","fileNames"],"sources":["../../src/cli/runSingleHarness.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { stdioDebugPreset, stdioJudgePreset } from '../presets/stdio.js';\n\nexport const MISSING_TEST_CASES_ERROR =\n 'a standard stdio problem (without judge.ts) needs at least one test case under test_cases/, static-analysis rules (e.g. requiredRegExpsInCode), or isManualScoringRequired';\n\n/**\n * Run the judge or debug harness of the problem in the current working directory: a custom\n * `judge.ts` / `debug.ts` when present, or the default stdio preset otherwise (mirroring the\n * Exercode server, which applies the stdio presets to problems without these files).\n */\nexport async function runSingleHarness(kind: 'judge' | 'debug'): Promise<number> {\n const problemDir = process.cwd();\n\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n console.error(formatDefaultHarnessError(defaultHarnessFileNames));\n return 1;\n }\n\n const harnessFileName = kind === 'judge' ? 'judge.ts' : 'debug.ts';\n if (fs.existsSync(path.join(problemDir, harnessFileName))) {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const spawnResult = child_process.spawnSync(process.execPath, ['run', harnessFileName, ...process.argv.slice(2)], {\n cwd: problemDir,\n stdio: 'inherit',\n });\n if (spawnResult.error) {\n console.error(`failed to run ${harnessFileName}: ${spawnResult.error.message}`);\n return 1;\n }\n return spawnResult.status ?? 1;\n }\n\n // A custom judge.ts marks the problem as non-standard, so the stdio debug preset would judge the\n // answer against a contract the problem does not have (mirroring the server, which reports debug\n // as unsupported in this case).\n if (kind === 'debug' && fs.existsSync(path.join(problemDir, 'judge.ts'))) {\n console.error('This problem has a custom judge.ts but no debug.ts; add a debug.ts to support debugging.');\n return 1;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (kind === 'judge') {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0 && !judgesWithoutTestCases(await readProblemMarkdownFrontMatter(problemDir))) {\n console.error(MISSING_TEST_CASES_ERROR);\n return 1;\n }\n }\n\n // The presets read the answer directory and params from `process.argv`, which holds them at the\n // same positions as when a harness script is invoked directly.\n await (kind === 'judge' ? stdioJudgePreset(problemDir) : stdioDebugPreset(problemDir));\n return 0;\n}\n\nexport function formatDefaultHarnessError(defaultHarnessFileNames: readonly string[]): string {\n const fileNames = defaultHarnessFileNames.join(' and ');\n return `${fileNames} must not be committed because the content is identical to the default stdio harness.\nA problem without judge.ts is automatically judged with stdioJudgePreset and debugged with stdioDebugPreset,\nso delete ${fileNames} and run the problem with \\`bun x exercode-problem judge <answerDir>\\` or \\`bun x exercode-problem debug <answerDir>\\`.\nIf the file intentionally demonstrates the default harness, add an explanatory comment to mark it as custom.`;\n}\n"],"mappings":"A,ucASA,MAAaiB,EACX,6KAOF,eAAsBC,EAAiB,EAA0C,CAC/E,IAAMC,EAAa,QAAQ,IAAI,EAEzBC,EAA0B,MAAMpB,EAAAA,6BAA6BmB,CAAU,EAC7E,GAAIC,EAAwB,OAAS,EAEnC,OADA,QAAQ,MAAMC,EAA0BD,CAAuB,CAAC,EACzD,EAGT,IAAME,EAAkBC,IAAS,QAAU,WAAa,WACxD,GAAItB,EAAAA,QAAG,WAAWC,EAAAA,QAAK,KAAKiB,EAAYG,CAAe,CAAC,EAAG,CAEzD,IAAME,EAAcrB,EAAAA,QAAc,UAAU,QAAQ,SAAU,CAAC,MAAOmB,EAAiB,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAG,CAChH,IAAKH,EACL,MAAO,SACT,CAAC,EAKD,OAJIK,EAAY,OACd,QAAQ,MAAM,iBAAiBF,EAAgB,IAAIE,EAAY,MAAM,SAAS,EACvE,GAEFA,EAAY,QAAU,CAC/B,CAwBA,OAnBID,IAAS,SAAWtB,EAAAA,QAAG,WAAWC,EAAAA,QAAK,KAAKiB,EAAY,UAAU,CAAC,GACrE,QAAQ,MAAM,0FAA0F,EACjG,GAMLI,IAAS,UAEP,MADoBnB,EAAAA,cAAcF,EAAAA,QAAK,KAAKiB,EAAY,YAAY,CAAC,EAAA,CAC3D,SAAW,GAAK,CAACd,EAAAA,uBAAuB,MAAMC,EAAAA,+BAA+Ba,CAAU,CAAC,GACpG,QAAQ,MAAMF,CAAwB,EAC/B,IAMX,MAAOM,IAAS,QAAUhB,EAAAA,iBAAiBY,CAAU,EAAIX,EAAAA,iBAAiBW,CAAU,GAC7E,EACT,CAEA,SAAgBE,EAA0B,EAAoD,CAC5F,IAAMI,EAAYL,EAAwB,KAAK,OAAO,EACtD,MAAO,GAAGK,EAAU;;YAEVA,EAAU;6GAEtB,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"runSingleHarness.js","names":["child_process","fs","path","childProcess","MISSING_TEST_CASES_ERROR","runSingleHarness","problemDir","defaultHarnessFileNames","findDefaultStdioHarnessFiles","formatDefaultHarnessError","harnessFileName","kind","spawnResult","readTestCases","judgesWithoutTestCases","readProblemMarkdownFrontMatter","stdioJudgePreset","stdioDebugPreset","fileNames"],"sources":["../../src/cli/runSingleHarness.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { stdioDebugPreset, stdioJudgePreset } from '../presets/stdio.js';\n\nexport const MISSING_TEST_CASES_ERROR =\n 'a standard stdio problem (without judge.ts) needs at least one test case under test_cases/, static-analysis rules (e.g. requiredRegExpsInCode), or isManualScoringRequired';\n\n/**\n * Run the judge or debug harness of the problem in the current working directory: a custom\n * `judge.ts` / `debug.ts` when present, or the default stdio preset otherwise (mirroring the\n * Exercode server, which applies the stdio presets to problems without these files).\n */\nexport async function runSingleHarness(kind: 'judge' | 'debug'): Promise<number> {\n const problemDir = process.cwd();\n\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n console.error(formatDefaultHarnessError(defaultHarnessFileNames));\n return 1;\n }\n\n const harnessFileName = kind === 'judge' ? 'judge.ts' : 'debug.ts';\n if (fs.existsSync(path.join(problemDir, harnessFileName))) {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const spawnResult = child_process.spawnSync(process.execPath, ['run', harnessFileName, ...process.argv.slice(2)], {\n cwd: problemDir,\n stdio: 'inherit',\n });\n if (spawnResult.error) {\n console.error(`failed to run ${harnessFileName}: ${spawnResult.error.message}`);\n return 1;\n }\n return spawnResult.status ?? 1;\n }\n\n // A custom judge.ts marks the problem as non-standard, so the stdio debug preset would judge the\n // answer against a contract the problem does not have (mirroring the server, which reports debug\n // as unsupported in this case).\n if (kind === 'debug' && fs.existsSync(path.join(problemDir, 'judge.ts'))) {\n console.error('This problem has a custom judge.ts but no debug.ts; add a debug.ts to support debugging.');\n return 1;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (kind === 'judge') {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0 && !judgesWithoutTestCases(await readProblemMarkdownFrontMatter(problemDir))) {\n console.error(MISSING_TEST_CASES_ERROR);\n return 1;\n }\n }\n\n // The presets read the answer directory and params from `process.argv`, which holds them at the\n // same positions as when a harness script is invoked directly.\n await (kind === 'judge' ? stdioJudgePreset(problemDir) : stdioDebugPreset(problemDir));\n return 0;\n}\n\nexport function formatDefaultHarnessError(defaultHarnessFileNames: readonly string[]): string {\n const fileNames = defaultHarnessFileNames.join(' and ');\n return `${fileNames} must not be committed because the content is identical to the default stdio harness.\nA problem without judge.ts is automatically judged with stdioJudgePreset and debugged with stdioDebugPreset,\nso delete ${fileNames} and run the problem with \\`bun x exercode-problem judge <answerDir>\\` or \\`bun x exercode-problem debug <answerDir>\\`.\nIf the file intentionally demonstrates the default harness, add an explanatory comment to mark it as custom.`;\n}\n"],"mappings":"saASA,MAAaI,EACX,6KAOF,eAAsBC,EAAiB,EAA0C,CAC/E,IAAMC,EAAa,QAAQ,IAAI,EAEzBC,EAA0B,MAAMC,EAA6BF,CAAU,EAC7E,GAAIC,EAAwB,OAAS,EAEnC,OADA,QAAQ,MAAME,EAA0BF,CAAuB,CAAC,EACzD,EAGT,IAAMG,EAAkBC,IAAS,QAAU,WAAa,WACxD,GAAIV,EAAG,WAAWC,EAAK,KAAKI,EAAYI,CAAe,CAAC,EAAG,CAEzD,IAAME,EAAcZ,EAAc,UAAU,QAAQ,SAAU,CAAC,MAAOU,EAAiB,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAG,CAChH,IAAKJ,EACL,MAAO,SACT,CAAC,EAKD,OAJIM,EAAY,OACd,QAAQ,MAAM,iBAAiBF,EAAgB,IAAIE,EAAY,MAAM,SAAS,EACvE,GAEFA,EAAY,QAAU,CAC/B,CAwBA,OAnBID,IAAS,SAAWV,EAAG,WAAWC,EAAK,KAAKI,EAAY,UAAU,CAAC,GACrE,QAAQ,MAAM,0FAA0F,EACjG,GAMLK,IAAS,UAEP,MADoBE,EAAcX,EAAK,KAAKI,EAAY,YAAY,CAAC,EAAA,CAC3D,SAAW,GAAK,CAACQ,EAAuB,MAAMC,EAA+BT,CAAU,CAAC,GACpG,QAAQ,MAAMF,CAAwB,EAC/B,IAMX,MAAOO,IAAS,QAAUK,EAAiBV,CAAU,EAAIW,EAAiBX,CAAU,GAC7E,EACT,CAEA,SAAgBG,EAA0B,EAAoD,CAC5F,IAAMS,EAAYX,EAAwB,KAAK,OAAO,EACtD,MAAO,GAAGW,EAAU;;YAEVA,EAAU;6GAEtB"}
1
+ {"version":3,"file":"runSingleHarness.js","names":["child_process","fs","path","childProcess","MISSING_TEST_CASES_ERROR","runSingleHarness","problemDir","defaultHarnessFileNames","findDefaultStdioHarnessFiles","formatDefaultHarnessError","harnessFileName","kind","spawnResult","readTestCases","judgesWithoutTestCases","readProblemMarkdownFrontMatter","stdioJudgePreset","stdioDebugPreset","fileNames"],"sources":["../../src/cli/runSingleHarness.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { stdioDebugPreset, stdioJudgePreset } from '../presets/stdio.js';\n\nexport const MISSING_TEST_CASES_ERROR =\n 'a standard stdio problem (without judge.ts) needs at least one test case under test_cases/, static-analysis rules (e.g. requiredRegExpsInCode), or isManualScoringRequired';\n\n/**\n * Run the judge or debug harness of the problem in the current working directory: a custom\n * `judge.ts` / `debug.ts` when present, or the default stdio preset otherwise (mirroring the\n * Exercode server, which applies the stdio presets to problems without these files).\n */\nexport async function runSingleHarness(kind: 'judge' | 'debug'): Promise<number> {\n const problemDir = process.cwd();\n\n const defaultHarnessFileNames = await findDefaultStdioHarnessFiles(problemDir);\n if (defaultHarnessFileNames.length > 0) {\n console.error(formatDefaultHarnessError(defaultHarnessFileNames));\n return 1;\n }\n\n const harnessFileName = kind === 'judge' ? 'judge.ts' : 'debug.ts';\n if (fs.existsSync(path.join(problemDir, harnessFileName))) {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const spawnResult = child_process.spawnSync(process.execPath, ['run', harnessFileName, ...process.argv.slice(2)], {\n cwd: problemDir,\n stdio: 'inherit',\n });\n if (spawnResult.error) {\n console.error(`failed to run ${harnessFileName}: ${spawnResult.error.message}`);\n return 1;\n }\n return spawnResult.status ?? 1;\n }\n\n // A custom judge.ts marks the problem as non-standard, so the stdio debug preset would judge the\n // answer against a contract the problem does not have (mirroring the server, which reports debug\n // as unsupported in this case).\n if (kind === 'debug' && fs.existsSync(path.join(problemDir, 'judge.ts'))) {\n console.error('This problem has a custom judge.ts but no debug.ts; add a debug.ts to support debugging.');\n return 1;\n }\n\n // Without test cases, stdioJudgePreset prints a single accepted sentinel result, so a standard\n // problem with an empty or missing test_cases/ would otherwise pass without being judged —\n // unless static-analysis rules or manual scoring make the problem judgeable without them.\n if (kind === 'judge') {\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n if (testCases.length === 0 && !judgesWithoutTestCases(await readProblemMarkdownFrontMatter(problemDir))) {\n console.error(MISSING_TEST_CASES_ERROR);\n return 1;\n }\n }\n\n // The presets read the answer directory and params from `process.argv`, which holds them at the\n // same positions as when a harness script is invoked directly.\n await (kind === 'judge' ? stdioJudgePreset(problemDir) : stdioDebugPreset(problemDir));\n return 0;\n}\n\nexport function formatDefaultHarnessError(defaultHarnessFileNames: readonly string[]): string {\n const fileNames = defaultHarnessFileNames.join(' and ');\n return `${fileNames} must not be committed because the content is identical to the default stdio harness.\nA problem without judge.ts is automatically judged with stdioJudgePreset and debugged with stdioDebugPreset,\nso delete ${fileNames} and run the problem with \\`bun x exercode-problem judge <answerDir>\\` or \\`bun x exercode-problem debug <answerDir>\\`.\nIf the file intentionally demonstrates the default harness, add an explanatory comment to mark it as custom.`;\n}\n"],"mappings":"A,saASA,MAAaI,EACX,6KAOF,eAAsBC,EAAiB,EAA0C,CAC/E,IAAMC,EAAa,QAAQ,IAAI,EAEzBC,EAA0B,MAAMC,EAA6BF,CAAU,EAC7E,GAAIC,EAAwB,OAAS,EAEnC,OADA,QAAQ,MAAME,EAA0BF,CAAuB,CAAC,EACzD,EAGT,IAAMG,EAAkBC,IAAS,QAAU,WAAa,WACxD,GAAIV,EAAG,WAAWC,EAAK,KAAKI,EAAYI,CAAe,CAAC,EAAG,CAEzD,IAAME,EAAcZ,EAAc,UAAU,QAAQ,SAAU,CAAC,MAAOU,EAAiB,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAG,CAChH,IAAKJ,EACL,MAAO,SACT,CAAC,EAKD,OAJIM,EAAY,OACd,QAAQ,MAAM,iBAAiBF,EAAgB,IAAIE,EAAY,MAAM,SAAS,EACvE,GAEFA,EAAY,QAAU,CAC/B,CAwBA,OAnBID,IAAS,SAAWV,EAAG,WAAWC,EAAK,KAAKI,EAAY,UAAU,CAAC,GACrE,QAAQ,MAAM,0FAA0F,EACjG,GAMLK,IAAS,UAEP,MADoBE,EAAcX,EAAK,KAAKI,EAAY,YAAY,CAAC,EAAA,CAC3D,SAAW,GAAK,CAACQ,EAAuB,MAAMC,EAA+BT,CAAU,CAAC,GACpG,QAAQ,MAAMF,CAAwB,EAC/B,IAMX,MAAOO,IAAS,QAAUK,EAAiBV,CAAU,EAAIW,EAAiBX,CAAU,GAC7E,EACT,CAEA,SAAgBG,EAA0B,EAAoD,CAC5F,IAAMS,EAAYX,EAAwB,KAAK,OAAO,EACtD,MAAO,GAAGW,EAAU;;YAEVA,EAAU;6GAEtB,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"validateLearningMaterial.cjs","names":["formatValidationReport","validateProblemDirectory","validateCourseDirectory","validateContestFile","require_validationResult","require_validateContest","require_validateCourse","require_validateProblem","VALIDATE_SUBCOMMANDS","validateLearningMaterial","parseValidateArgs","args","targets","subcommand","problemsDir","hasError","target","result","validate","index","arg"],"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":"0SAKaQ,EAAuB,CAAC,mBAAoB,kBAAmB,kBAAkB,EAQ9F,eAAsBC,EACpB,EACA,EACiB,CACjB,GAAM,CAAE,UAAS,eAAgBC,EAAkBC,CAAI,EACvD,GAAIC,EAAQ,SAAW,EAAG,MAAU,MAAM,GAAGC,EAAW,4BAA4B,EACpF,GAAIA,IAAe,mBACb,IAAAC,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGD,EAAW,8BAA8B,CAAA,MACtF,GAAID,EAAQ,OAAS,EAC1B,MAAU,MAAM,GAAGC,EAAW,qCAAqCD,EAAQ,QAAQ,EAGrF,IAAIG,EAAW,GACf,IAAK,IAAMC,KAAUJ,EAAS,CAC5B,IAAMK,EAAS,MAAMC,EAASL,EAAYG,EAAQF,CAAW,EAC7D,QAAQ,OAAO,MAAMd,EAAAA,uBAAuBgB,EAAQC,CAAM,CAAC,EAC3D,IAAaA,EAAO,OAAO,OAAS,CACtC,CACA,MAAO,IACT,CAEA,eAAeC,EACb,EACA,EACA,EAC2B,CAC3B,OAAQL,EAAR,CACE,IAAK,mBACH,OAAOZ,EAAAA,yBAAyBe,CAAM,EAExC,IAAK,kBACH,OAAOd,EAAAA,wBAAwBc,EAAQ,CAAE,uBAAsB,CAAC,EAElE,IAAK,mBACH,OAAOb,EAAAA,oBAAoBa,EAAQ,CAAE,uBAAsB,CAAC,CAEhE,CACF,CAEA,SAASN,EAAkB,EAAiF,CAC1G,IAAME,EAAoB,CAAC,EACvBE,EACJ,IAAK,IAAIK,EAAQ,EAAGA,EAAQR,EAAK,OAAQ,IAAS,CAChD,IAAMS,EAAMT,EAAKQ,GACjB,GAAIC,IAAQ,IAAA,GAAW,MACvB,GAAIA,IAAQ,iBAEV,IADA,EAAcT,EAAK,EAAEQ,GACjBL,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGM,EAAI,kBAAkB,CAAA,MACnE,GAAIA,EAAI,WAAW,IAAI,EAC5B,MAAU,MAAM,mBAAmBA,GAAK,OAExCR,EAAQ,KAAKQ,CAAG,CAEpB,CACA,MAAO,CAAE,UAAS,aAAY,CAChC"}
1
+ {"version":3,"file":"validateLearningMaterial.cjs","names":["formatValidationReport","validateProblemDirectory","validateCourseDirectory","validateContestFile","require_validationResult","require_validateContest","require_validateCourse","require_validateProblem","VALIDATE_SUBCOMMANDS","validateLearningMaterial","parseValidateArgs","args","targets","subcommand","problemsDir","hasError","target","result","validate","index","arg"],"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":"A,0SAKaQ,EAAuB,CAAC,mBAAoB,kBAAmB,kBAAkB,EAQ9F,eAAsBC,EACpB,EACA,EACiB,CACjB,GAAM,CAAE,UAAS,eAAgBC,EAAkBC,CAAI,EACvD,GAAIC,EAAQ,SAAW,EAAG,MAAU,MAAM,GAAGC,EAAW,4BAA4B,EACpF,GAAIA,IAAe,mBACb,IAAAC,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGD,EAAW,8BAA8B,CAAA,MACtF,GAAID,EAAQ,OAAS,EAC1B,MAAU,MAAM,GAAGC,EAAW,qCAAqCD,EAAQ,QAAQ,EAGrF,IAAIG,EAAW,GACf,IAAK,IAAMC,KAAUJ,EAAS,CAC5B,IAAMK,EAAS,MAAMC,EAASL,EAAYG,EAAQF,CAAW,EAC7D,QAAQ,OAAO,MAAMd,EAAAA,uBAAuBgB,EAAQC,CAAM,CAAC,EAC3D,IAAaA,EAAO,OAAO,OAAS,CACtC,CACA,MAAO,IACT,CAEA,eAAeC,EACb,EACA,EACA,EAC2B,CAC3B,OAAQL,EAAR,CACE,IAAK,mBACH,OAAOZ,EAAAA,yBAAyBe,CAAM,EAExC,IAAK,kBACH,OAAOd,EAAAA,wBAAwBc,EAAQ,CAAE,uBAAsB,CAAC,EAElE,IAAK,mBACH,OAAOb,EAAAA,oBAAoBa,EAAQ,CAAE,uBAAsB,CAAC,CAEhE,CACF,CAEA,SAASN,EAAkB,EAAiF,CAC1G,IAAME,EAAoB,CAAC,EACvBE,EACJ,IAAK,IAAIK,EAAQ,EAAGA,EAAQR,EAAK,OAAQ,IAAS,CAChD,IAAMS,EAAMT,EAAKQ,GACjB,GAAIC,IAAQ,IAAA,GAAW,MACvB,GAAIA,IAAQ,iBAEV,IADA,EAAcT,EAAK,EAAEQ,GACjBL,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGM,EAAI,kBAAkB,CAAA,MACnE,GAAIA,EAAI,WAAW,IAAI,EAC5B,MAAU,MAAM,mBAAmBA,GAAK,OAExCR,EAAQ,KAAKQ,CAAG,CAEpB,CACA,MAAO,CAAE,UAAS,aAAY,CAChC,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"validateLearningMaterial.js","names":["VALIDATE_SUBCOMMANDS","validateLearningMaterial","parseValidateArgs","args","targets","subcommand","problemsDir","hasError","target","result","validate","formatValidationReport","validateProblemDirectory","validateCourseDirectory","validateContestFile","index","arg"],"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,MAAaA,EAAuB,CAAC,mBAAoB,kBAAmB,kBAAkB,EAQ9F,eAAsBC,EACpB,EACA,EACiB,CACjB,GAAM,CAAE,UAAS,eAAgBC,EAAkBC,CAAI,EACvD,GAAIC,EAAQ,SAAW,EAAG,MAAU,MAAM,GAAGC,EAAW,4BAA4B,EACpF,GAAIA,IAAe,mBACb,IAAAC,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGD,EAAW,8BAA8B,CAAA,MACtF,GAAID,EAAQ,OAAS,EAC1B,MAAU,MAAM,GAAGC,EAAW,qCAAqCD,EAAQ,QAAQ,EAGrF,IAAIG,EAAW,GACf,IAAK,IAAMC,KAAUJ,EAAS,CAC5B,IAAMK,EAAS,MAAMC,EAASL,EAAYG,EAAQF,CAAW,EAC7D,QAAQ,OAAO,MAAMK,EAAuBH,EAAQC,CAAM,CAAC,EAC3D,IAAaA,EAAO,OAAO,OAAS,CACtC,CACA,MAAO,IACT,CAEA,eAAeC,EACb,EACA,EACA,EAC2B,CAC3B,OAAQL,EAAR,CACE,IAAK,mBACH,OAAOO,EAAyBJ,CAAM,EAExC,IAAK,kBACH,OAAOK,EAAwBL,EAAQ,CAAE,uBAAsB,CAAC,EAElE,IAAK,mBACH,OAAOM,EAAoBN,EAAQ,CAAE,uBAAsB,CAAC,CAEhE,CACF,CAEA,SAASN,EAAkB,EAAiF,CAC1G,IAAME,EAAoB,CAAC,EACvBE,EACJ,IAAK,IAAIS,EAAQ,EAAGA,EAAQZ,EAAK,OAAQ,IAAS,CAChD,IAAMa,EAAMb,EAAKY,GACjB,GAAIC,IAAQ,IAAA,GAAW,MACvB,GAAIA,IAAQ,iBAEV,IADA,EAAcb,EAAK,EAAEY,GACjBT,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGU,EAAI,kBAAkB,CAAA,MACnE,GAAIA,EAAI,WAAW,IAAI,EAC5B,MAAU,MAAM,mBAAmBA,GAAK,OAExCZ,EAAQ,KAAKY,CAAG,CAEpB,CACA,MAAO,CAAE,UAAS,aAAY,CAChC"}
1
+ {"version":3,"file":"validateLearningMaterial.js","names":["VALIDATE_SUBCOMMANDS","validateLearningMaterial","parseValidateArgs","args","targets","subcommand","problemsDir","hasError","target","result","validate","formatValidationReport","validateProblemDirectory","validateCourseDirectory","validateContestFile","index","arg"],"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":"A,gUAKA,MAAaA,EAAuB,CAAC,mBAAoB,kBAAmB,kBAAkB,EAQ9F,eAAsBC,EACpB,EACA,EACiB,CACjB,GAAM,CAAE,UAAS,eAAgBC,EAAkBC,CAAI,EACvD,GAAIC,EAAQ,SAAW,EAAG,MAAU,MAAM,GAAGC,EAAW,4BAA4B,EACpF,GAAIA,IAAe,mBACb,IAAAC,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGD,EAAW,8BAA8B,CAAA,MACtF,GAAID,EAAQ,OAAS,EAC1B,MAAU,MAAM,GAAGC,EAAW,qCAAqCD,EAAQ,QAAQ,EAGrF,IAAIG,EAAW,GACf,IAAK,IAAMC,KAAUJ,EAAS,CAC5B,IAAMK,EAAS,MAAMC,EAASL,EAAYG,EAAQF,CAAW,EAC7D,QAAQ,OAAO,MAAMK,EAAuBH,EAAQC,CAAM,CAAC,EAC3D,IAAaA,EAAO,OAAO,OAAS,CACtC,CACA,MAAO,IACT,CAEA,eAAeC,EACb,EACA,EACA,EAC2B,CAC3B,OAAQL,EAAR,CACE,IAAK,mBACH,OAAOO,EAAyBJ,CAAM,EAExC,IAAK,kBACH,OAAOK,EAAwBL,EAAQ,CAAE,uBAAsB,CAAC,EAElE,IAAK,mBACH,OAAOM,EAAoBN,EAAQ,CAAE,uBAAsB,CAAC,CAEhE,CACF,CAEA,SAASN,EAAkB,EAAiF,CAC1G,IAAME,EAAoB,CAAC,EACvBE,EACJ,IAAK,IAAIS,EAAQ,EAAGA,EAAQZ,EAAK,OAAQ,IAAS,CAChD,IAAMa,EAAMb,EAAKY,GACjB,GAAIC,IAAQ,IAAA,GAAW,MACvB,GAAIA,IAAQ,iBAEV,IADA,EAAcb,EAAK,EAAEY,GACjBT,IAAgB,IAAA,GAAW,MAAU,MAAM,GAAGU,EAAI,kBAAkB,CAAA,MACnE,GAAIA,EAAI,WAAW,IAAI,EAC5B,MAAU,MAAM,mBAAmBA,GAAK,OAExCZ,EAAQ,KAAKY,CAAG,CAEpB,CACA,MAAO,CAAE,UAAS,aAAY,CAChC,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"checkProblemDirIsolation.cjs","names":["copyProblemDirToTemporaryRoot","path","isRecord","runHarnessProcess","getErrorMessage","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","DecisionCode","require_runtime","require_testCaseResult","require_temporaryProblemDirCopy","require_decisionCode","require_printDebugBanner","require_runHarnessProcess","node_path","_willbooster_shared_lib","ISOLATION_CHECK_MIN_TIMEOUT_MS","ISOLATION_CHECK_OVERHEAD_MS","checkProblemDirIsolation","timeoutMs","options","tempRoot","copyResult","problemDir","absoluteProblemDir","relativeCwd","resolvedCwd","copiedCwd","copiedProblemDir","scriptPath","getInvokedScriptPath","execArgv","isIsolationExecArg","paramsJson","params","result","isAcceptedJudgeOutput","describeFailure","error","arg","relativeScriptPath","resultLines","stdout","line","parsedResult"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { getErrorMessage, isRecord } from '@willbooster/shared-lib';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\nimport { type HarnessProcessResult, runHarnessProcess } from './runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from './temporaryProblemDirCopy.js';\n\nconst ISOLATION_CHECK_MIN_TIMEOUT_MS = 30_000;\n// Starting bun and printing results are not covered by the judge's own limits.\nconst ISOLATION_CHECK_OVERHEAD_MS = 10_000;\nconst ISOLATION_CHECK_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nexport interface ProblemDirIsolationCheckOptions {\n /**\n * The judge's declared worst-case duration for one accepted submission (build timeout plus the\n * time limit of every test case). The check allows at least that plus a fixed overhead; per-case\n * process startup and custom judge callbacks are not budgeted separately.\n */\n expectedMaxDurationMs?: number;\n}\n\nexport interface ProblemDirIsolationCheckResult {\n passed: boolean;\n}\n\n/**\n * Check that a judge can run after only the problem directory is copied elsewhere.\n */\nexport async function checkProblemDirIsolation(\n problemDir: string,\n resolvedCwd: ResolvedCwd,\n params: unknown,\n options: ProblemDirIsolationCheckOptions = {}\n): Promise<ProblemDirIsolationCheckResult> {\n const timeoutMs = Math.max(\n ISOLATION_CHECK_MIN_TIMEOUT_MS,\n (options.expectedMaxDurationMs ?? 0) + ISOLATION_CHECK_OVERHEAD_MS\n );\n let tempRoot: string | undefined;\n try {\n const copyResult = await copyProblemDirToTemporaryRoot(problemDir);\n tempRoot = copyResult.tempRoot;\n const { copiedProblemDir } = copyResult;\n const absoluteProblemDir = path.resolve(problemDir);\n\n const relativeCwd = path.relative(absoluteProblemDir, path.resolve(resolvedCwd.cwd));\n const copiedCwd = path.join(copiedProblemDir, relativeCwd);\n const scriptPath = getInvokedScriptPath(absoluteProblemDir);\n if (scriptPath.startsWith('..') || path.isAbsolute(scriptPath)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'The invoked judge script is located outside the problem directory.',\n `Script path: ${scriptPath}`,\n ]);\n return { passed: true };\n }\n const execArgv = process.execArgv.filter(isIsolationExecArg);\n const paramsJson = JSON.stringify(isRecord(params) ? params : {});\n const result = await runHarnessProcess([...execArgv, scriptPath, copiedCwd, paramsJson], {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs,\n maxOutputBytes: ISOLATION_CHECK_MAX_OUTPUT_BYTES,\n tempRoot,\n });\n\n if (result.exitCode === 0 && isAcceptedJudgeOutput(result.stdout)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check passed',\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n ]);\n return { passed: true };\n }\n\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed',\n '',\n ...describeFailure(result, timeoutMs, options),\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n `Exit status : ${result.exitCode ?? result.signal ?? 'unknown'}`,\n `Failure reason : ${result.failureReason ?? '<none>'}`,\n '',\n 'stdout:',\n result.stdout.trimEnd() || '<empty>',\n '',\n 'stderr:',\n result.stderr.trimEnd() || '<empty>',\n ]);\n return { passed: false };\n } catch (error) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed due to an unexpected error',\n '',\n getErrorMessage(error),\n ]);\n return { passed: false };\n } finally {\n // Cleanup failures must not mask the primary isolation check result.\n if (tempRoot) await forciblyRemoveDirectory(tempRoot);\n }\n}\n\nfunction describeFailure(\n result: HarnessProcessResult,\n timeoutMs: number,\n options: ProblemDirIsolationCheckOptions\n): string[] {\n if (result.timedOut) {\n return [\n `The judge did not finish within ${timeoutMs / 1000} seconds after copying only the problem directory to a temporary location.`,\n ...(options.expectedMaxDurationMs === undefined\n ? [\n `This judge declares no run time, so the check uses its fixed ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000}-second budget.`,\n ]\n : [\n `The budget is the larger of ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000} seconds and the build timeout plus the time limit of every test case plus a ${ISOLATION_CHECK_OVERHEAD_MS / 1000}-second overhead.`,\n 'Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.',\n ]),\n ];\n }\n if (result.failureReason !== undefined) {\n return ['The judge was stopped before it finished; see the failure reason below.'];\n }\n if (result.exitCode === undefined) {\n return [\n `The judge was terminated by ${result.signal ?? 'an unknown signal'} (e.g. an out-of-memory kill or a cancelled job).`,\n ];\n }\n return [\n 'The judge did not complete successfully after copying only the problem directory to a temporary location.',\n 'Make sure judge.ts imports only files included in the problem directory.',\n ];\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction getInvokedScriptPath(problemDir: string): string {\n const scriptPath = process.argv[1];\n if (!scriptPath) return `.${path.sep}judge.ts`;\n const relativeScriptPath = path.relative(problemDir, path.resolve(scriptPath));\n if (path.isAbsolute(relativeScriptPath)) return relativeScriptPath;\n return relativeScriptPath.startsWith('.') ? relativeScriptPath : `.${path.sep}${relativeScriptPath}`;\n}\n\nfunction isAcceptedJudgeOutput(stdout: string): boolean {\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n if (resultLines.length === 0) return false;\n\n return resultLines.every((line) => {\n try {\n const parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n return parsedResult.success && parsedResult.data.decisionCode === DecisionCode.ACCEPTED;\n } catch {\n return false;\n }\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"checkProblemDirIsolation.cjs","names":["copyProblemDirToTemporaryRoot","path","isRecord","runHarnessProcess","getErrorMessage","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","DecisionCode","require_runtime","require_testCaseResult","require_temporaryProblemDirCopy","require_decisionCode","require_printDebugBanner","require_runHarnessProcess","node_path","_willbooster_shared_lib","ISOLATION_CHECK_MIN_TIMEOUT_MS","ISOLATION_CHECK_OVERHEAD_MS","checkProblemDirIsolation","timeoutMs","options","tempRoot","copyResult","problemDir","absoluteProblemDir","relativeCwd","resolvedCwd","copiedCwd","copiedProblemDir","scriptPath","getInvokedScriptPath","execArgv","isIsolationExecArg","paramsJson","params","result","isAcceptedJudgeOutput","describeFailure","error","arg","relativeScriptPath","resultLines","stdout","line","parsedResult"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { getErrorMessage, isRecord } from '@willbooster/shared-lib';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\nimport { type HarnessProcessResult, runHarnessProcess } from './runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from './temporaryProblemDirCopy.js';\n\nconst ISOLATION_CHECK_MIN_TIMEOUT_MS = 30_000;\n// Starting bun and printing results are not covered by the judge's own limits.\nconst ISOLATION_CHECK_OVERHEAD_MS = 10_000;\nconst ISOLATION_CHECK_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nexport interface ProblemDirIsolationCheckOptions {\n /**\n * The judge's declared worst-case duration for one accepted submission (build timeout plus the\n * time limit of every test case). The check allows at least that plus a fixed overhead; per-case\n * process startup and custom judge callbacks are not budgeted separately.\n */\n expectedMaxDurationMs?: number;\n}\n\nexport interface ProblemDirIsolationCheckResult {\n passed: boolean;\n}\n\n/**\n * Check that a judge can run after only the problem directory is copied elsewhere.\n */\nexport async function checkProblemDirIsolation(\n problemDir: string,\n resolvedCwd: ResolvedCwd,\n params: unknown,\n options: ProblemDirIsolationCheckOptions = {}\n): Promise<ProblemDirIsolationCheckResult> {\n const timeoutMs = Math.max(\n ISOLATION_CHECK_MIN_TIMEOUT_MS,\n (options.expectedMaxDurationMs ?? 0) + ISOLATION_CHECK_OVERHEAD_MS\n );\n let tempRoot: string | undefined;\n try {\n const copyResult = await copyProblemDirToTemporaryRoot(problemDir);\n tempRoot = copyResult.tempRoot;\n const { copiedProblemDir } = copyResult;\n const absoluteProblemDir = path.resolve(problemDir);\n\n const relativeCwd = path.relative(absoluteProblemDir, path.resolve(resolvedCwd.cwd));\n const copiedCwd = path.join(copiedProblemDir, relativeCwd);\n const scriptPath = getInvokedScriptPath(absoluteProblemDir);\n if (scriptPath.startsWith('..') || path.isAbsolute(scriptPath)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'The invoked judge script is located outside the problem directory.',\n `Script path: ${scriptPath}`,\n ]);\n return { passed: true };\n }\n const execArgv = process.execArgv.filter(isIsolationExecArg);\n const paramsJson = JSON.stringify(isRecord(params) ? params : {});\n const result = await runHarnessProcess([...execArgv, scriptPath, copiedCwd, paramsJson], {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs,\n maxOutputBytes: ISOLATION_CHECK_MAX_OUTPUT_BYTES,\n tempRoot,\n });\n\n if (result.exitCode === 0 && isAcceptedJudgeOutput(result.stdout)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check passed',\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n ]);\n return { passed: true };\n }\n\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed',\n '',\n ...describeFailure(result, timeoutMs, options),\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n `Exit status : ${result.exitCode ?? result.signal ?? 'unknown'}`,\n `Failure reason : ${result.failureReason ?? '<none>'}`,\n '',\n 'stdout:',\n result.stdout.trimEnd() || '<empty>',\n '',\n 'stderr:',\n result.stderr.trimEnd() || '<empty>',\n ]);\n return { passed: false };\n } catch (error) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed due to an unexpected error',\n '',\n getErrorMessage(error),\n ]);\n return { passed: false };\n } finally {\n // Cleanup failures must not mask the primary isolation check result.\n if (tempRoot) await forciblyRemoveDirectory(tempRoot);\n }\n}\n\nfunction describeFailure(\n result: HarnessProcessResult,\n timeoutMs: number,\n options: ProblemDirIsolationCheckOptions\n): string[] {\n if (result.timedOut) {\n return [\n `The judge did not finish within ${timeoutMs / 1000} seconds after copying only the problem directory to a temporary location.`,\n ...(options.expectedMaxDurationMs === undefined\n ? [\n `This judge declares no run time, so the check uses its fixed ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000}-second budget.`,\n ]\n : [\n `The budget is the larger of ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000} seconds and the build timeout plus the time limit of every test case plus a ${ISOLATION_CHECK_OVERHEAD_MS / 1000}-second overhead.`,\n 'Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.',\n ]),\n ];\n }\n if (result.failureReason !== undefined) {\n return ['The judge was stopped before it finished; see the failure reason below.'];\n }\n if (result.exitCode === undefined) {\n return [\n `The judge was terminated by ${result.signal ?? 'an unknown signal'} (e.g. an out-of-memory kill or a cancelled job).`,\n ];\n }\n return [\n 'The judge did not complete successfully after copying only the problem directory to a temporary location.',\n 'Make sure judge.ts imports only files included in the problem directory.',\n ];\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction getInvokedScriptPath(problemDir: string): string {\n const scriptPath = process.argv[1];\n if (!scriptPath) return `.${path.sep}judge.ts`;\n const relativeScriptPath = path.relative(problemDir, path.resolve(scriptPath));\n if (path.isAbsolute(relativeScriptPath)) return relativeScriptPath;\n return relativeScriptPath.startsWith('.') ? relativeScriptPath : `.${path.sep}${relativeScriptPath}`;\n}\n\nfunction isAcceptedJudgeOutput(stdout: string): boolean {\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n if (resultLines.length === 0) return false;\n\n return resultLines.every((line) => {\n try {\n const parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n return parsedResult.success && parsedResult.data.decisionCode === DecisionCode.ACCEPTED;\n } catch {\n return false;\n }\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"checkProblemDirIsolation.js","names":["path","ISOLATION_CHECK_MIN_TIMEOUT_MS","ISOLATION_CHECK_OVERHEAD_MS","checkProblemDirIsolation","timeoutMs","options","tempRoot","copyResult","copyProblemDirToTemporaryRoot","problemDir","absoluteProblemDir","relativeCwd","resolvedCwd","copiedCwd","copiedProblemDir","scriptPath","getInvokedScriptPath","printDebugBanner","execArgv","isIsolationExecArg","paramsJson","isRecord","params","result","runHarnessProcess","isAcceptedJudgeOutput","describeFailure","error","getErrorMessage","forciblyRemoveDirectory","arg","relativeScriptPath","resultLines","stdout","line","TEST_CASE_RESULT_PREFIX","parsedResult","testCaseResultSchema","DecisionCode"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { getErrorMessage, isRecord } from '@willbooster/shared-lib';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\nimport { type HarnessProcessResult, runHarnessProcess } from './runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from './temporaryProblemDirCopy.js';\n\nconst ISOLATION_CHECK_MIN_TIMEOUT_MS = 30_000;\n// Starting bun and printing results are not covered by the judge's own limits.\nconst ISOLATION_CHECK_OVERHEAD_MS = 10_000;\nconst ISOLATION_CHECK_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nexport interface ProblemDirIsolationCheckOptions {\n /**\n * The judge's declared worst-case duration for one accepted submission (build timeout plus the\n * time limit of every test case). The check allows at least that plus a fixed overhead; per-case\n * process startup and custom judge callbacks are not budgeted separately.\n */\n expectedMaxDurationMs?: number;\n}\n\nexport interface ProblemDirIsolationCheckResult {\n passed: boolean;\n}\n\n/**\n * Check that a judge can run after only the problem directory is copied elsewhere.\n */\nexport async function checkProblemDirIsolation(\n problemDir: string,\n resolvedCwd: ResolvedCwd,\n params: unknown,\n options: ProblemDirIsolationCheckOptions = {}\n): Promise<ProblemDirIsolationCheckResult> {\n const timeoutMs = Math.max(\n ISOLATION_CHECK_MIN_TIMEOUT_MS,\n (options.expectedMaxDurationMs ?? 0) + ISOLATION_CHECK_OVERHEAD_MS\n );\n let tempRoot: string | undefined;\n try {\n const copyResult = await copyProblemDirToTemporaryRoot(problemDir);\n tempRoot = copyResult.tempRoot;\n const { copiedProblemDir } = copyResult;\n const absoluteProblemDir = path.resolve(problemDir);\n\n const relativeCwd = path.relative(absoluteProblemDir, path.resolve(resolvedCwd.cwd));\n const copiedCwd = path.join(copiedProblemDir, relativeCwd);\n const scriptPath = getInvokedScriptPath(absoluteProblemDir);\n if (scriptPath.startsWith('..') || path.isAbsolute(scriptPath)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'The invoked judge script is located outside the problem directory.',\n `Script path: ${scriptPath}`,\n ]);\n return { passed: true };\n }\n const execArgv = process.execArgv.filter(isIsolationExecArg);\n const paramsJson = JSON.stringify(isRecord(params) ? params : {});\n const result = await runHarnessProcess([...execArgv, scriptPath, copiedCwd, paramsJson], {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs,\n maxOutputBytes: ISOLATION_CHECK_MAX_OUTPUT_BYTES,\n tempRoot,\n });\n\n if (result.exitCode === 0 && isAcceptedJudgeOutput(result.stdout)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check passed',\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n ]);\n return { passed: true };\n }\n\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed',\n '',\n ...describeFailure(result, timeoutMs, options),\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n `Exit status : ${result.exitCode ?? result.signal ?? 'unknown'}`,\n `Failure reason : ${result.failureReason ?? '<none>'}`,\n '',\n 'stdout:',\n result.stdout.trimEnd() || '<empty>',\n '',\n 'stderr:',\n result.stderr.trimEnd() || '<empty>',\n ]);\n return { passed: false };\n } catch (error) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed due to an unexpected error',\n '',\n getErrorMessage(error),\n ]);\n return { passed: false };\n } finally {\n // Cleanup failures must not mask the primary isolation check result.\n if (tempRoot) await forciblyRemoveDirectory(tempRoot);\n }\n}\n\nfunction describeFailure(\n result: HarnessProcessResult,\n timeoutMs: number,\n options: ProblemDirIsolationCheckOptions\n): string[] {\n if (result.timedOut) {\n return [\n `The judge did not finish within ${timeoutMs / 1000} seconds after copying only the problem directory to a temporary location.`,\n ...(options.expectedMaxDurationMs === undefined\n ? [\n `This judge declares no run time, so the check uses its fixed ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000}-second budget.`,\n ]\n : [\n `The budget is the larger of ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000} seconds and the build timeout plus the time limit of every test case plus a ${ISOLATION_CHECK_OVERHEAD_MS / 1000}-second overhead.`,\n 'Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.',\n ]),\n ];\n }\n if (result.failureReason !== undefined) {\n return ['The judge was stopped before it finished; see the failure reason below.'];\n }\n if (result.exitCode === undefined) {\n return [\n `The judge was terminated by ${result.signal ?? 'an unknown signal'} (e.g. an out-of-memory kill or a cancelled job).`,\n ];\n }\n return [\n 'The judge did not complete successfully after copying only the problem directory to a temporary location.',\n 'Make sure judge.ts imports only files included in the problem directory.',\n ];\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction getInvokedScriptPath(problemDir: string): string {\n const scriptPath = process.argv[1];\n if (!scriptPath) return `.${path.sep}judge.ts`;\n const relativeScriptPath = path.relative(problemDir, path.resolve(scriptPath));\n if (path.isAbsolute(relativeScriptPath)) return relativeScriptPath;\n return relativeScriptPath.startsWith('.') ? relativeScriptPath : `.${path.sep}${relativeScriptPath}`;\n}\n\nfunction isAcceptedJudgeOutput(stdout: string): boolean {\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n if (resultLines.length === 0) return false;\n\n return resultLines.every((line) => {\n try {\n const parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n return parsedResult.success && parsedResult.data.decisionCode === DecisionCode.ACCEPTED;\n } catch {\n return false;\n }\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"checkProblemDirIsolation.js","names":["path","ISOLATION_CHECK_MIN_TIMEOUT_MS","ISOLATION_CHECK_OVERHEAD_MS","checkProblemDirIsolation","timeoutMs","options","tempRoot","copyResult","copyProblemDirToTemporaryRoot","problemDir","absoluteProblemDir","relativeCwd","resolvedCwd","copiedCwd","copiedProblemDir","scriptPath","getInvokedScriptPath","printDebugBanner","execArgv","isIsolationExecArg","paramsJson","isRecord","params","result","runHarnessProcess","isAcceptedJudgeOutput","describeFailure","error","getErrorMessage","forciblyRemoveDirectory","arg","relativeScriptPath","resultLines","stdout","line","TEST_CASE_RESULT_PREFIX","parsedResult","testCaseResultSchema","DecisionCode"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { getErrorMessage, isRecord } from '@willbooster/shared-lib';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\nimport { type HarnessProcessResult, runHarnessProcess } from './runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from './temporaryProblemDirCopy.js';\n\nconst ISOLATION_CHECK_MIN_TIMEOUT_MS = 30_000;\n// Starting bun and printing results are not covered by the judge's own limits.\nconst ISOLATION_CHECK_OVERHEAD_MS = 10_000;\nconst ISOLATION_CHECK_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;\n\nexport interface ProblemDirIsolationCheckOptions {\n /**\n * The judge's declared worst-case duration for one accepted submission (build timeout plus the\n * time limit of every test case). The check allows at least that plus a fixed overhead; per-case\n * process startup and custom judge callbacks are not budgeted separately.\n */\n expectedMaxDurationMs?: number;\n}\n\nexport interface ProblemDirIsolationCheckResult {\n passed: boolean;\n}\n\n/**\n * Check that a judge can run after only the problem directory is copied elsewhere.\n */\nexport async function checkProblemDirIsolation(\n problemDir: string,\n resolvedCwd: ResolvedCwd,\n params: unknown,\n options: ProblemDirIsolationCheckOptions = {}\n): Promise<ProblemDirIsolationCheckResult> {\n const timeoutMs = Math.max(\n ISOLATION_CHECK_MIN_TIMEOUT_MS,\n (options.expectedMaxDurationMs ?? 0) + ISOLATION_CHECK_OVERHEAD_MS\n );\n let tempRoot: string | undefined;\n try {\n const copyResult = await copyProblemDirToTemporaryRoot(problemDir);\n tempRoot = copyResult.tempRoot;\n const { copiedProblemDir } = copyResult;\n const absoluteProblemDir = path.resolve(problemDir);\n\n const relativeCwd = path.relative(absoluteProblemDir, path.resolve(resolvedCwd.cwd));\n const copiedCwd = path.join(copiedProblemDir, relativeCwd);\n const scriptPath = getInvokedScriptPath(absoluteProblemDir);\n if (scriptPath.startsWith('..') || path.isAbsolute(scriptPath)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'The invoked judge script is located outside the problem directory.',\n `Script path: ${scriptPath}`,\n ]);\n return { passed: true };\n }\n const execArgv = process.execArgv.filter(isIsolationExecArg);\n const paramsJson = JSON.stringify(isRecord(params) ? params : {});\n const result = await runHarnessProcess([...execArgv, scriptPath, copiedCwd, paramsJson], {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs,\n maxOutputBytes: ISOLATION_CHECK_MAX_OUTPUT_BYTES,\n tempRoot,\n });\n\n if (result.exitCode === 0 && isAcceptedJudgeOutput(result.stdout)) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check passed',\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n ]);\n return { passed: true };\n }\n\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed',\n '',\n ...describeFailure(result, timeoutMs, options),\n '',\n `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n `Exit status : ${result.exitCode ?? result.signal ?? 'unknown'}`,\n `Failure reason : ${result.failureReason ?? '<none>'}`,\n '',\n 'stdout:',\n result.stdout.trimEnd() || '<empty>',\n '',\n 'stderr:',\n result.stderr.trimEnd() || '<empty>',\n ]);\n return { passed: false };\n } catch (error) {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check failed due to an unexpected error',\n '',\n getErrorMessage(error),\n ]);\n return { passed: false };\n } finally {\n // Cleanup failures must not mask the primary isolation check result.\n if (tempRoot) await forciblyRemoveDirectory(tempRoot);\n }\n}\n\nfunction describeFailure(\n result: HarnessProcessResult,\n timeoutMs: number,\n options: ProblemDirIsolationCheckOptions\n): string[] {\n if (result.timedOut) {\n return [\n `The judge did not finish within ${timeoutMs / 1000} seconds after copying only the problem directory to a temporary location.`,\n ...(options.expectedMaxDurationMs === undefined\n ? [\n `This judge declares no run time, so the check uses its fixed ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000}-second budget.`,\n ]\n : [\n `The budget is the larger of ${ISOLATION_CHECK_MIN_TIMEOUT_MS / 1000} seconds and the build timeout plus the time limit of every test case plus a ${ISOLATION_CHECK_OVERHEAD_MS / 1000}-second overhead.`,\n 'Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.',\n ]),\n ];\n }\n if (result.failureReason !== undefined) {\n return ['The judge was stopped before it finished; see the failure reason below.'];\n }\n if (result.exitCode === undefined) {\n return [\n `The judge was terminated by ${result.signal ?? 'an unknown signal'} (e.g. an out-of-memory kill or a cancelled job).`,\n ];\n }\n return [\n 'The judge did not complete successfully after copying only the problem directory to a temporary location.',\n 'Make sure judge.ts imports only files included in the problem directory.',\n ];\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction getInvokedScriptPath(problemDir: string): string {\n const scriptPath = process.argv[1];\n if (!scriptPath) return `.${path.sep}judge.ts`;\n const relativeScriptPath = path.relative(problemDir, path.resolve(scriptPath));\n if (path.isAbsolute(relativeScriptPath)) return relativeScriptPath;\n return relativeScriptPath.startsWith('.') ? relativeScriptPath : `.${path.sep}${relativeScriptPath}`;\n}\n\nfunction isAcceptedJudgeOutput(stdout: string): boolean {\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n if (resultLines.length === 0) return false;\n\n return resultLines.every((line) => {\n try {\n const parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n return parsedResult.success && parsedResult.data.decisionCode === DecisionCode.ACCEPTED;\n } catch {\n return false;\n }\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cleanWorkingDirectory.cjs","names":["fs","path","require_runtime","node_path","node_fs_promises","snapshotWorkingDirectory","paths","cwd","cleanWorkingDirectory","cleanDirectory","snapshot","entry","dir","relativePath","relativePrefix"],"sources":["../../src/helpers/cleanWorkingDirectory.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\n// Currently, it does not support changing file contents and deleting files.\nexport async function snapshotWorkingDirectory(cwd: string): Promise<ReadonlySet<string>> {\n const paths = await fs.readdir(cwd, { recursive: true });\n return new Set(paths);\n}\n\nexport async function cleanWorkingDirectory(cwd: string, snapshot: ReadonlySet<string>): Promise<void> {\n await cleanDirectory(cwd, '', snapshot);\n}\n\nasync function cleanDirectory(dir: string, relativePrefix: string, snapshot: ReadonlySet<string>): Promise<void> {\n for (const entry of await fs.readdir(dir, { withFileTypes: true })) {\n const relativePath = path.join(relativePrefix, entry.name);\n if (!snapshot.has(relativePath)) {\n // An unsnapshotted directory is removed whole, so its contents are never visited.\n await fs.rm(path.join(dir, entry.name), { force: true, recursive: true });\n } else if (entry.isDirectory()) {\n await cleanDirectory(path.join(dir, entry.name), relativePath, snapshot);\n }\n }\n}\n"],"mappings":"iKAIA,eAAsBK,EAAyB,EAA2C,CACxF,IAAMC,EAAQ,MAAMN,EAAAA,QAAG,QAAQO,EAAK,CAAE,UAAW,EAAK,CAAC,EACvD,OAAO,IAAI,IAAID,CAAK,CACtB,CAEA,eAAsBE,EAAsB,EAAa,EAA8C,CACrG,MAAMC,EAAeF,EAAK,GAAIG,CAAQ,CACxC,CAEA,eAAeD,EAAe,EAAa,EAAwB,EAA8C,CAC/G,IAAK,IAAME,KAAS,MAAMX,EAAAA,QAAG,QAAQY,EAAK,CAAE,cAAe,EAAK,CAAC,EAAG,CAClE,IAAMC,EAAeZ,EAAAA,QAAK,KAAKa,EAAgBH,EAAM,IAAI,EACpDD,EAAS,IAAIG,CAAY,EAGnBF,EAAM,YAAY,GAC3B,MAAMF,EAAeR,EAAAA,QAAK,KAAKW,EAAKD,EAAM,IAAI,EAAGE,EAAcH,CAAQ,EAFvE,MAAMV,EAAAA,QAAG,GAAGC,EAAAA,QAAK,KAAKW,EAAKD,EAAM,IAAI,EAAG,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,CAI5E,CACF"}
1
+ {"version":3,"file":"cleanWorkingDirectory.cjs","names":["fs","path","require_runtime","node_path","node_fs_promises","snapshotWorkingDirectory","paths","cwd","cleanWorkingDirectory","cleanDirectory","snapshot","entry","dir","relativePath","relativePrefix"],"sources":["../../src/helpers/cleanWorkingDirectory.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\n// Currently, it does not support changing file contents and deleting files.\nexport async function snapshotWorkingDirectory(cwd: string): Promise<ReadonlySet<string>> {\n const paths = await fs.readdir(cwd, { recursive: true });\n return new Set(paths);\n}\n\nexport async function cleanWorkingDirectory(cwd: string, snapshot: ReadonlySet<string>): Promise<void> {\n await cleanDirectory(cwd, '', snapshot);\n}\n\nasync function cleanDirectory(dir: string, relativePrefix: string, snapshot: ReadonlySet<string>): Promise<void> {\n for (const entry of await fs.readdir(dir, { withFileTypes: true })) {\n const relativePath = path.join(relativePrefix, entry.name);\n if (!snapshot.has(relativePath)) {\n // An unsnapshotted directory is removed whole, so its contents are never visited.\n await fs.rm(path.join(dir, entry.name), { force: true, recursive: true });\n } else if (entry.isDirectory()) {\n await cleanDirectory(path.join(dir, entry.name), relativePath, snapshot);\n }\n }\n}\n"],"mappings":"A,iKAIA,eAAsBK,EAAyB,EAA2C,CACxF,IAAMC,EAAQ,MAAMN,EAAAA,QAAG,QAAQO,EAAK,CAAE,UAAW,EAAK,CAAC,EACvD,OAAO,IAAI,IAAID,CAAK,CACtB,CAEA,eAAsBE,EAAsB,EAAa,EAA8C,CACrG,MAAMC,EAAeF,EAAK,GAAIG,CAAQ,CACxC,CAEA,eAAeD,EAAe,EAAa,EAAwB,EAA8C,CAC/G,IAAK,IAAME,KAAS,MAAMX,EAAAA,QAAG,QAAQY,EAAK,CAAE,cAAe,EAAK,CAAC,EAAG,CAClE,IAAMC,EAAeZ,EAAAA,QAAK,KAAKa,EAAgBH,EAAM,IAAI,EACpDD,EAAS,IAAIG,CAAY,EAGnBF,EAAM,YAAY,GAC3B,MAAMF,EAAeR,EAAAA,QAAK,KAAKW,EAAKD,EAAM,IAAI,EAAGE,EAAcH,CAAQ,EAFvE,MAAMV,EAAAA,QAAG,GAAGC,EAAAA,QAAK,KAAKW,EAAKD,EAAM,IAAI,EAAG,CAAE,MAAO,GAAM,UAAW,EAAK,CAAC,CAI5E,CACF,C"}