@exercode/problem-utils 2.2.2 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.js +1 -1
  4. package/dist/cli/checkAllProblems.js.map +1 -1
  5. package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
  6. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  7. package/dist/helpers/checkProblemDirIsolation.d.ts +9 -16
  8. package/dist/helpers/checkProblemDirIsolation.js +1 -1
  9. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  10. package/dist/helpers/judgeByStaticAnalysis.cjs +7 -8
  11. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  12. package/dist/helpers/judgeByStaticAnalysis.js +7 -8
  13. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  14. package/dist/helpers/runHarnessProcess.cjs +2 -0
  15. package/dist/helpers/runHarnessProcess.cjs.map +1 -0
  16. package/dist/helpers/runHarnessProcess.d.ts +23 -0
  17. package/dist/helpers/runHarnessProcess.js +2 -0
  18. package/dist/helpers/runHarnessProcess.js.map +1 -0
  19. package/dist/helpers/runIsolationCheckInDebugMode.cjs +2 -0
  20. package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -0
  21. package/dist/helpers/runIsolationCheckInDebugMode.d.ts +7 -0
  22. package/dist/helpers/runIsolationCheckInDebugMode.js +2 -0
  23. package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -0
  24. package/dist/helpers/temporaryProblemDirCopy.cjs +2 -0
  25. package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -0
  26. package/dist/helpers/temporaryProblemDirCopy.d.ts +15 -0
  27. package/dist/helpers/temporaryProblemDirCopy.js +2 -0
  28. package/dist/helpers/temporaryProblemDirCopy.js.map +1 -0
  29. package/dist/index.cjs +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +1 -1
  32. package/dist/presets/command.cjs +1 -1
  33. package/dist/presets/command.cjs.map +1 -1
  34. package/dist/presets/command.js +1 -1
  35. package/dist/presets/command.js.map +1 -1
  36. package/dist/presets/evaluation.cjs +4 -4
  37. package/dist/presets/evaluation.cjs.map +1 -1
  38. package/dist/presets/evaluation.js +4 -4
  39. package/dist/presets/evaluation.js.map +1 -1
  40. package/dist/types/problem.cjs +1 -1
  41. package/dist/types/problem.cjs.map +1 -1
  42. package/dist/types/problem.d.ts +26 -3
  43. package/dist/types/problem.js +1 -1
  44. package/dist/types/problem.js.map +1 -1
  45. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/defaultStdioHarness.cjs"),n=require("../types/testCaseResult.cjs"),r=require("../types/decisionCode.cjs"),i=require("../helpers/checkProblemDirIsolation.cjs"),a=require("../helpers/readProblemMarkdownFrontMatter.cjs"),o=require("../helpers/readTestCases.cjs"),s=require("../helpers/findModelAnswerDirs.cjs"),c=require("./runSingleHarness.cjs");let l=require("node:fs/promises");l=e.__toESM(l,1);let u=require("node:path");u=e.__toESM(u,1);let d=require("node:child_process");d=e.__toESM(d,1);const f=6e5,p=67108864,m=1e3,h=new Map(Object.entries(r.DecisionCode).map(([e,t])=>[t,e]));async function g(e){let n=T(e),r=u.default.resolve(n.rootDir),i=e=>(u.default.relative(r,e)||`.`).replaceAll(u.default.sep,`/`),l=await E(r),d=l.filter(e=>{let t=i(e);return n.only.length>0&&!n.only.some(e=>t.includes(e))?!1:!n.skip.some(e=>t.includes(e))});if(d.length===0)return console.error(l.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${r}.`:`All ${l.length} problem directories under ${r} were excluded by --only/--skip.`),1;let f=[],p=[];for(let e of d){let n=await t.findDefaultStdioHarnessFiles(e);if(n.length>0){f.push(`${i(e)}: ${c.formatDefaultHarnessError(n)}`);continue}let r=await s.findModelAnswerDirs(e);if(r.length===0){f.push(`${i(e)}: no model answers found under model_answers/`);continue}if(!await O(u.default.join(e,`judge.ts`))&&(await o.readTestCases(u.default.join(e,`test_cases`))).length===0){let t;try{t=await a.readProblemMarkdownFrontMatter(e)}catch(t){f.push(`${i(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!a.judgesWithoutTestCases(t)){f.push(`${i(e)}: ${c.MISSING_TEST_CASES_ERROR}`);continue}}let l=await s.findFailingModelAnswerDirs(e);p.push(...r.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...l.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of f)console.error(`✗ ${e}`);let m=u.default.resolve(process.argv[1]??``),h=0,g=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(n.concurrency,p.length))},async()=>{for(;g<p.length;){let e=p[g++];if(!e)return;let t=`${i(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await _(e,m);n===void 0?(h++,console.info(`✓ ${t}`)):(f.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${h} passed, ${f.length} failed (${p.length} runs, ${d.length} problems)`),f.length===0?0:1}async function _(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return k(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let a,o;try{a=v(e,await x([`run`,t,`judge`,u.default.relative(e.problemDir,e.answerDir)],r,n))}catch(e){a=k(`harness execution failed: ${e instanceof Error?e.message:String(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:k(`${a}; ${s}`)}function v(e,t){let{stdout:i,stderr:a}=t;if(t.failureReason!==void 0)return k(t.failureReason);if(t.exitCode!==0)return k(`harness exited with ${t.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let o=i.split(/\r?\n/).filter(e=>e.startsWith(n.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of o){let t;try{t=n.testCaseResultSchema.safeParse(JSON.parse(e.slice(n.TEST_CASE_RESULT_PREFIX.length)))}catch{t=void 0}if(!t?.success)return k(`malformed test case result line: ${e}`);s.push(t.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==r.DecisionCode.ACCEPTED);return e?k(`${h.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===r.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}const y=new Set;let b=!1;function x(e,t,n){return S(),new Promise(r=>{let i=d.default.spawn(process.execPath,e,{cwd:t,detached:process.platform!==`win32`,env:w(),stdio:[`ignore`,`pipe`,`pipe`]}),a=i.pid===void 0?void 0:{pid:i.pid,tempRoot:n};a&&y.add(a);let o=[],s=[],c=0,l,u=!1,m=e=>{if(l===void 0){l=e,i.stdout?.destroy(),i.stderr?.destroy();try{i.pid===void 0?i.kill(`SIGKILL`):C(i.pid)}catch{i.kill(`SIGKILL`)}}},h=setTimeout(()=>m(`timed out after ${f/1e3} seconds`),f),g=(e,t)=>{if(c+=e.byteLength,c>p){m(`the harness printed more than ${p/1024/1024} MB of output`);return}t.push(e)};i.stdout?.on(`data`,e=>g(e,o)),i.stderr?.on(`data`,e=>g(e,s));let _=(e,t)=>{u||(u=!0,clearTimeout(h),a&&y.delete(a),r({stdout:Buffer.concat(o).toString(`utf8`),stderr:Buffer.concat(s).toString(`utf8`),exitCode:e,failureReason:l??(t?`failed to run the harness: ${t.message}`:void 0)}))};i.on(`error`,e=>_(void 0,e)),i.on(`close`,e=>_(e??void 0))})}function S(){if(!b){b=!0;for(let e of[`SIGINT`,`SIGTERM`])process.once(e,()=>{for(let e of y){try{C(e.pid)}catch{}i.forciblyRemoveDirectorySync(e.tempRoot)}process.kill(process.pid,e)})}}function C(e){process.platform===`win32`?d.default.spawnSync(`taskkill`,[`/PID`,String(e),`/T`,`/F`],{stdio:`ignore`}):process.kill(-e,`SIGKILL`)}function w(){let e={...process.env};return delete e.EXERCODE_SANDBOX_USER,e}function T(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 E(e){let t=[];return await D(e,t),t.toSorted()}async function D(e,t){let n=await l.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await D(u.default.join(e,r.name),t)}async function O(e){try{return await l.default.stat(e),!0}catch{return!1}}function k(e){return e.length<=m?e:`${e.slice(0,m)}...`}exports.checkAllProblems=g;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/defaultStdioHarness.cjs"),n=require("../types/testCaseResult.cjs"),r=require("../types/decisionCode.cjs"),i=require("../helpers/temporaryProblemDirCopy.cjs"),a=require("../helpers/runHarnessProcess.cjs"),o=require("../helpers/readProblemMarkdownFrontMatter.cjs"),s=require("../helpers/readTestCases.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("./runSingleHarness.cjs");let u=require("node:fs/promises");u=e.__toESM(u,1);let d=require("node:path");d=e.__toESM(d,1);const f=1e3,p=new Map(Object.entries(r.DecisionCode).map(([e,t])=>[t,e]));async function m(e){let n=v(e),r=d.default.resolve(n.rootDir),i=e=>(d.default.relative(r,e)||`.`).replaceAll(d.default.sep,`/`),a=await y(r),u=a.filter(e=>{let t=i(e);return n.only.length>0&&!n.only.some(e=>t.includes(e))?!1:!n.skip.some(e=>t.includes(e))});if(u.length===0)return console.error(a.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${r}.`:`All ${a.length} problem directories under ${r} were excluded by --only/--skip.`),1;let f=[],p=[];for(let e of u){let n=await t.findDefaultStdioHarnessFiles(e);if(n.length>0){f.push(`${i(e)}: ${l.formatDefaultHarnessError(n)}`);continue}let r=await c.findModelAnswerDirs(e);if(r.length===0){f.push(`${i(e)}: no model answers found under model_answers/`);continue}if(!await x(d.default.join(e,`judge.ts`))&&(await s.readTestCases(d.default.join(e,`test_cases`))).length===0){let t;try{t=await o.readProblemMarkdownFrontMatter(e)}catch(t){f.push(`${i(e)}: failed to read the problem markdown front matter: ${t instanceof Error?t.message:String(t)}`);continue}if(!o.judgesWithoutTestCases(t)){f.push(`${i(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let a=await c.findFailingModelAnswerDirs(e);p.push(...r.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...a.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of f)console.error(`✗ ${e}`);let m=d.default.resolve(process.argv[1]??``),g=0,_=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(n.concurrency,p.length))},async()=>{for(;_<p.length;){let e=p[_++];if(!e)return;let t=`${i(e.problemDir)} ${d.default.relative(e.problemDir,e.answerDir).replaceAll(d.default.sep,`/`)}`,n=await h(e,m);n===void 0?(g++,console.info(`✓ ${t}`)):(f.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${g} passed, ${f.length} failed (${p.length} runs, ${u.length} problems)`),f.length===0?0:1}async function h(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return S(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let o,s;try{o=g(e,await a.runHarnessProcess([`run`,t,`judge`,d.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:_(),timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=S(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{s=await i.forciblyRemoveDirectory(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:S(`${o}; ${c}`)}function g(e,t){let{stdout:i,stderr:a}=t;if(t.failureReason!==void 0)return S(t.failureReason);if(t.exitCode!==0)return S(`harness exited with ${t.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let o=i.split(/\r?\n/).filter(e=>e.startsWith(n.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of o){let t;try{t=n.testCaseResultSchema.safeParse(JSON.parse(e.slice(n.TEST_CASE_RESULT_PREFIX.length)))}catch{t=void 0}if(!t?.success)return S(`malformed test case result line: ${e}`);s.push(t.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==r.DecisionCode.ACCEPTED);return e?S(`${p.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===r.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function _(){let e={...process.env};return delete e.EXERCODE_SANDBOX_USER,e}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 u.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await b(d.default.join(e,r.name),t)}async function x(e){try{return await u.default.stat(e),!0}catch{return!1}}function S(e){return e.length<=f?e:`${e.slice(0,f)}...`}exports.checkAllProblems=m;
2
2
  //# sourceMappingURL=checkAllProblems.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","copyProblemDirToTemporaryRoot","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","child_process","fs"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport {\n copyProblemDirToTemporaryRoot,\n forciblyRemoveDirectory,\n forciblyRemoveDirectorySync,\n} from '../helpers/checkProblemDirIsolation.js';\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 { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\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: ${error instanceof Error ? error.message : String(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 let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\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 );\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(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\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 copiedProblemDir,\n tempRoot\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\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\ninterface HarnessProcessResult {\n stdout: string;\n stderr: string;\n exitCode: number | undefined;\n failureReason: string | undefined;\n}\n\ninterface LiveHarnessRun {\n pid: number;\n tempRoot: string;\n}\n\n// Detached harness groups no longer receive the terminal's SIGINT, so an interrupted check run must\n// tear them down (and remove their temp copies) itself before exiting.\nconst liveHarnessRuns = new Set<LiveHarnessRun>();\nlet signalHandlersInstalled = false;\n\n/**\n * Run the harness in its own process group, killing the whole group on timeout or when the output\n * cap is exceeded so grandchild submission processes cannot outlive the run.\n */\nfunction runHarnessProcess(\n commandArgs: readonly string[],\n cwd: string,\n tempRoot: string\n): Promise<HarnessProcessResult> {\n installSignalHandlers();\n return new Promise((resolve) => {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const child = child_process.spawn(process.execPath, commandArgs, {\n cwd,\n detached: process.platform !== 'win32',\n env: createHarnessEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const liveRun: LiveHarnessRun | undefined = child.pid === undefined ? undefined : { pid: child.pid, tempRoot };\n if (liveRun) liveHarnessRuns.add(liveRun);\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let totalOutputBytes = 0;\n let failureReason: string | undefined;\n let settled = false;\n\n const killProcessGroup = (reason: string): void => {\n if (failureReason !== undefined) return;\n failureReason = reason;\n // Stop buffering immediately: OS pipe buffers can keep emitting data after the kill, and\n // destroyed streams also let `close` fire even if a stray grandchild inherited the pipes.\n child.stdout?.destroy();\n child.stderr?.destroy();\n try {\n if (child.pid === undefined) {\n child.kill('SIGKILL');\n } else {\n killHarnessTree(child.pid);\n }\n } catch {\n child.kill('SIGKILL');\n }\n };\n const timeoutId = setTimeout(\n () => killProcessGroup(`timed out after ${RUN_TIMEOUT_MS / 1000} seconds`),\n RUN_TIMEOUT_MS\n );\n\n const appendOutput = (chunk: Buffer, chunks: Buffer[]): void => {\n totalOutputBytes += chunk.byteLength;\n if (totalOutputBytes > MAX_RUN_OUTPUT_BYTES) {\n killProcessGroup(`the harness printed more than ${MAX_RUN_OUTPUT_BYTES / 1024 / 1024} MB of output`);\n return;\n }\n chunks.push(chunk);\n };\n child.stdout?.on('data', (chunk: Buffer) => appendOutput(chunk, stdoutChunks));\n child.stderr?.on('data', (chunk: Buffer) => appendOutput(chunk, stderrChunks));\n\n const settle = (exitCode: number | undefined, spawnError?: Error): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n if (liveRun) liveHarnessRuns.delete(liveRun);\n resolve({\n stdout: Buffer.concat(stdoutChunks).toString('utf8'),\n stderr: Buffer.concat(stderrChunks).toString('utf8'),\n exitCode,\n failureReason: failureReason ?? (spawnError ? `failed to run the harness: ${spawnError.message}` : undefined),\n });\n };\n child.on('error', (error) => settle(undefined, error));\n child.on('close', (exitCode) => settle(exitCode ?? undefined));\n });\n}\n\nfunction installSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, () => {\n for (const liveRun of liveHarnessRuns) {\n try {\n killHarnessTree(liveRun.pid);\n } catch {\n // The tree already exited.\n }\n // Best-effort cleanup while exiting; judged code may have left permission-locked entries.\n forciblyRemoveDirectorySync(liveRun.tempRoot);\n }\n process.kill(process.pid, signal);\n });\n }\n}\n\n// On Windows the harness is not detached (process groups are unavailable), so killing only the\n// direct child would leave submission grandchildren running; taskkill terminates the whole tree.\nfunction killHarnessTree(pid: number): void {\n if (process.platform === 'win32') {\n child_process.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });\n } else {\n process.kill(-pid, 'SIGKILL');\n }\n}\n\n// The all-problem check runs harnesses concurrently and has no sandbox-delegation contract, while the sandbox\n// helpers assume one harness at a time (their pkill targets every process of the sandbox user), so\n// never forward the sandbox user to judged runs.\nfunction createHarnessEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.EXERCODE_SANDBOX_USER;\n return env;\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","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","fs"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\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';\n\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: ${error instanceof Error ? error.message : String(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 let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\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 );\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(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\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: createHarnessEnv(),\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(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\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\n// The all-problem check runs harnesses concurrently and has no sandbox-delegation contract, while the sandbox\n// helpers assume one harness at a time (their pkill targets every process of the sandbox user), so\n// never forward the sandbox user to judged runs.\nfunction createHarnessEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.EXERCODE_SANDBOX_USER;\n return env;\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,2 +1,2 @@
1
- import{findDefaultStdioHarnessFiles as e}from"../helpers/defaultStdioHarness.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"../types/testCaseResult.js";import{DecisionCode as r}from"../types/decisionCode.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a,forciblyRemoveDirectorySync as o}from"../helpers/checkProblemDirIsolation.js";import{judgesWithoutTestCases as s,readProblemMarkdownFrontMatter as c}from"../helpers/readProblemMarkdownFrontMatter.js";import{readTestCases as l}from"../helpers/readTestCases.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:fs/promises";import h from"node:path";import g from"node:child_process";const _=6e5,v=67108864,y=1e3,b=new Map(Object.entries(r).map(([e,t])=>[t,e]));async function x(t){let n=A(t),r=h.resolve(n.rootDir),i=e=>(h.relative(r,e)||`.`).replaceAll(h.sep,`/`),a=await j(r),o=a.filter(e=>{let t=i(e);return n.only.length>0&&!n.only.some(e=>t.includes(e))?!1:!n.skip.some(e=>t.includes(e))});if(o.length===0)return console.error(a.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${r}.`:`All ${a.length} problem directories under ${r} were excluded by --only/--skip.`),1;let m=[],g=[];for(let t of o){let n=await e(t);if(n.length>0){m.push(`${i(t)}: ${p(n)}`);continue}let r=await d(t);if(r.length===0){m.push(`${i(t)}: no model answers found under model_answers/`);continue}if(!await N(h.join(t,`judge.ts`))&&(await l(h.join(t,`test_cases`))).length===0){let e;try{e=await c(t)}catch(e){m.push(`${i(t)}: failed to read the problem markdown front matter: ${e instanceof Error?e.message:String(e)}`);continue}if(!s(e)){m.push(`${i(t)}: ${f}`);continue}}let a=await u(t);g.push(...r.map(e=>({problemDir:t,answerDir:e,expectation:`accepted`})),...a.map(e=>({problemDir:t,answerDir:e,expectation:`rejected`})))}for(let e of m)console.error(`✗ ${e}`);let _=h.resolve(process.argv[1]??``),v=0,y=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(n.concurrency,g.length))},async()=>{for(;y<g.length;){let e=g[y++];if(!e)return;let t=`${i(e.problemDir)} ${h.relative(e.problemDir,e.answerDir).replaceAll(h.sep,`/`)}`,n=await S(e,_);n===void 0?(v++,console.info(`✓ ${t}`)):(m.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${v} passed, ${m.length} failed (${g.length} runs, ${o.length} problems)`),m.length===0?0:1}async function S(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return P(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let o,s;try{o=C(e,await E([`run`,t,`judge`,h.relative(e.problemDir,e.answerDir)],r,n))}catch(e){o=P(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{s=await a(n)}if(s)return o;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return o===void 0?c:P(`${o}; ${c}`)}function C(e,i){let{stdout:a,stderr:o}=i;if(i.failureReason!==void 0)return P(i.failureReason);if(i.exitCode!==0)return P(`harness exited with ${i.exitCode??`a signal`}${o.trim()?`: ${o.trim()}`:``}`);let s=a.split(/\r?\n/).filter(e=>e.startsWith(t)),c=[];for(let e of s){let r;try{r=n.safeParse(JSON.parse(e.slice(t.length)))}catch{r=void 0}if(!r?.success)return P(`malformed test case result line: ${e}`);c.push(r.data)}if(c.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=c.find(e=>e.decisionCode!==r.ACCEPTED);return e?P(`${b.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return c.every(e=>e.decisionCode===r.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}const w=new Set;let T=!1;function E(e,t,n){return D(),new Promise(r=>{let i=g.spawn(process.execPath,e,{cwd:t,detached:process.platform!==`win32`,env:k(),stdio:[`ignore`,`pipe`,`pipe`]}),a=i.pid===void 0?void 0:{pid:i.pid,tempRoot:n};a&&w.add(a);let o=[],s=[],c=0,l,u=!1,d=e=>{if(l===void 0){l=e,i.stdout?.destroy(),i.stderr?.destroy();try{i.pid===void 0?i.kill(`SIGKILL`):O(i.pid)}catch{i.kill(`SIGKILL`)}}},f=setTimeout(()=>d(`timed out after ${_/1e3} seconds`),_),p=(e,t)=>{if(c+=e.byteLength,c>v){d(`the harness printed more than ${v/1024/1024} MB of output`);return}t.push(e)};i.stdout?.on(`data`,e=>p(e,o)),i.stderr?.on(`data`,e=>p(e,s));let m=(e,t)=>{u||(u=!0,clearTimeout(f),a&&w.delete(a),r({stdout:Buffer.concat(o).toString(`utf8`),stderr:Buffer.concat(s).toString(`utf8`),exitCode:e,failureReason:l??(t?`failed to run the harness: ${t.message}`:void 0)}))};i.on(`error`,e=>m(void 0,e)),i.on(`close`,e=>m(e??void 0))})}function D(){if(!T){T=!0;for(let e of[`SIGINT`,`SIGTERM`])process.once(e,()=>{for(let e of w){try{O(e.pid)}catch{}o(e.tempRoot)}process.kill(process.pid,e)})}}function O(e){process.platform===`win32`?g.spawnSync(`taskkill`,[`/PID`,String(e),`/T`,`/F`],{stdio:`ignore`}):process.kill(-e,`SIGKILL`)}function k(){let e={...process.env};return delete e.EXERCODE_SANDBOX_USER,e}function A(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 j(e){let t=[];return await M(e,t),t.toSorted()}async function M(e,t){let n=await m.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await M(h.join(e,r.name),t)}async function N(e){try{return await m.stat(e),!0}catch{return!1}}function P(e){return e.length<=y?e:`${e.slice(0,y)}...`}export{x as checkAllProblems};
1
+ import{findDefaultStdioHarnessFiles as e}from"../helpers/defaultStdioHarness.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"../types/testCaseResult.js";import{DecisionCode as r}from"../types/decisionCode.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"../helpers/temporaryProblemDirCopy.js";import{runHarnessProcess as o}from"../helpers/runHarnessProcess.js";import{judgesWithoutTestCases as s,readProblemMarkdownFrontMatter as c}from"../helpers/readProblemMarkdownFrontMatter.js";import{readTestCases as l}from"../helpers/readTestCases.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:fs/promises";import h from"node:path";const g=1e3,_=new Map(Object.entries(r).map(([e,t])=>[t,e]));async function v(t){let n=S(t),r=h.resolve(n.rootDir),i=e=>(h.relative(r,e)||`.`).replaceAll(h.sep,`/`),a=await C(r),o=a.filter(e=>{let t=i(e);return n.only.length>0&&!n.only.some(e=>t.includes(e))?!1:!n.skip.some(e=>t.includes(e))});if(o.length===0)return console.error(a.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${r}.`:`All ${a.length} problem directories under ${r} were excluded by --only/--skip.`),1;let m=[],g=[];for(let t of o){let n=await e(t);if(n.length>0){m.push(`${i(t)}: ${p(n)}`);continue}let r=await d(t);if(r.length===0){m.push(`${i(t)}: no model answers found under model_answers/`);continue}if(!await T(h.join(t,`judge.ts`))&&(await l(h.join(t,`test_cases`))).length===0){let e;try{e=await c(t)}catch(e){m.push(`${i(t)}: failed to read the problem markdown front matter: ${e instanceof Error?e.message:String(e)}`);continue}if(!s(e)){m.push(`${i(t)}: ${f}`);continue}}let a=await u(t);g.push(...r.map(e=>({problemDir:t,answerDir:e,expectation:`accepted`})),...a.map(e=>({problemDir:t,answerDir:e,expectation:`rejected`})))}for(let e of m)console.error(`✗ ${e}`);let _=h.resolve(process.argv[1]??``),v=0,b=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(n.concurrency,g.length))},async()=>{for(;b<g.length;){let e=g[b++];if(!e)return;let t=`${i(e.problemDir)} ${h.relative(e.problemDir,e.answerDir).replaceAll(h.sep,`/`)}`,n=await y(e,_);n===void 0?(v++,console.info(`✓ ${t}`)):(m.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}})),console.info(`\n${v} passed, ${m.length} failed (${g.length} runs, ${o.length} problems)`),m.length===0?0:1}async function y(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return E(`failed to copy the problem directory to a temporary location: ${e instanceof Error?e.message:String(e)}`)}let s,c;try{s=b(e,await o([`run`,t,`judge`,h.relative(e.problemDir,e.answerDir)],{cwd:r,env:x(),timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){s=E(`harness execution failed: ${e instanceof Error?e.message:String(e)}`)}finally{c=await a(n)}if(c)return s;let l=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return s===void 0?l:E(`${s}; ${l}`)}function b(e,i){let{stdout:a,stderr:o}=i;if(i.failureReason!==void 0)return E(i.failureReason);if(i.exitCode!==0)return E(`harness exited with ${i.exitCode??`a signal`}${o.trim()?`: ${o.trim()}`:``}`);let s=a.split(/\r?\n/).filter(e=>e.startsWith(t)),c=[];for(let e of s){let r;try{r=n.safeParse(JSON.parse(e.slice(t.length)))}catch{r=void 0}if(!r?.success)return E(`malformed test case result line: ${e}`);c.push(r.data)}if(c.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=c.find(e=>e.decisionCode!==r.ACCEPTED);return e?E(`${_.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return c.every(e=>e.decisionCode===r.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function x(){let e={...process.env};return delete e.EXERCODE_SANDBOX_USER,e}function S(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 C(e){let t=[];return await w(e,t),t.toSorted()}async function w(e,t){let n=await m.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)!r.isDirectory()||r.name===`node_modules`||r.name.startsWith(`.`)||await w(h.join(e,r.name),t)}async function T(e){try{return await m.stat(e),!0}catch{return!1}}function E(e){return e.length<=g?e:`${e.slice(0,g)}...`}export{v as checkAllProblems};
2
2
  //# sourceMappingURL=checkAllProblems.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkAllProblems.js","names":[],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport {\n copyProblemDirToTemporaryRoot,\n forciblyRemoveDirectory,\n forciblyRemoveDirectorySync,\n} from '../helpers/checkProblemDirIsolation.js';\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 { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\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: ${error instanceof Error ? error.message : String(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 let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\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 );\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(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\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 copiedProblemDir,\n tempRoot\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\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\ninterface HarnessProcessResult {\n stdout: string;\n stderr: string;\n exitCode: number | undefined;\n failureReason: string | undefined;\n}\n\ninterface LiveHarnessRun {\n pid: number;\n tempRoot: string;\n}\n\n// Detached harness groups no longer receive the terminal's SIGINT, so an interrupted check run must\n// tear them down (and remove their temp copies) itself before exiting.\nconst liveHarnessRuns = new Set<LiveHarnessRun>();\nlet signalHandlersInstalled = false;\n\n/**\n * Run the harness in its own process group, killing the whole group on timeout or when the output\n * cap is exceeded so grandchild submission processes cannot outlive the run.\n */\nfunction runHarnessProcess(\n commandArgs: readonly string[],\n cwd: string,\n tempRoot: string\n): Promise<HarnessProcessResult> {\n installSignalHandlers();\n return new Promise((resolve) => {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const child = child_process.spawn(process.execPath, commandArgs, {\n cwd,\n detached: process.platform !== 'win32',\n env: createHarnessEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const liveRun: LiveHarnessRun | undefined = child.pid === undefined ? undefined : { pid: child.pid, tempRoot };\n if (liveRun) liveHarnessRuns.add(liveRun);\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let totalOutputBytes = 0;\n let failureReason: string | undefined;\n let settled = false;\n\n const killProcessGroup = (reason: string): void => {\n if (failureReason !== undefined) return;\n failureReason = reason;\n // Stop buffering immediately: OS pipe buffers can keep emitting data after the kill, and\n // destroyed streams also let `close` fire even if a stray grandchild inherited the pipes.\n child.stdout?.destroy();\n child.stderr?.destroy();\n try {\n if (child.pid === undefined) {\n child.kill('SIGKILL');\n } else {\n killHarnessTree(child.pid);\n }\n } catch {\n child.kill('SIGKILL');\n }\n };\n const timeoutId = setTimeout(\n () => killProcessGroup(`timed out after ${RUN_TIMEOUT_MS / 1000} seconds`),\n RUN_TIMEOUT_MS\n );\n\n const appendOutput = (chunk: Buffer, chunks: Buffer[]): void => {\n totalOutputBytes += chunk.byteLength;\n if (totalOutputBytes > MAX_RUN_OUTPUT_BYTES) {\n killProcessGroup(`the harness printed more than ${MAX_RUN_OUTPUT_BYTES / 1024 / 1024} MB of output`);\n return;\n }\n chunks.push(chunk);\n };\n child.stdout?.on('data', (chunk: Buffer) => appendOutput(chunk, stdoutChunks));\n child.stderr?.on('data', (chunk: Buffer) => appendOutput(chunk, stderrChunks));\n\n const settle = (exitCode: number | undefined, spawnError?: Error): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n if (liveRun) liveHarnessRuns.delete(liveRun);\n resolve({\n stdout: Buffer.concat(stdoutChunks).toString('utf8'),\n stderr: Buffer.concat(stderrChunks).toString('utf8'),\n exitCode,\n failureReason: failureReason ?? (spawnError ? `failed to run the harness: ${spawnError.message}` : undefined),\n });\n };\n child.on('error', (error) => settle(undefined, error));\n child.on('close', (exitCode) => settle(exitCode ?? undefined));\n });\n}\n\nfunction installSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, () => {\n for (const liveRun of liveHarnessRuns) {\n try {\n killHarnessTree(liveRun.pid);\n } catch {\n // The tree already exited.\n }\n // Best-effort cleanup while exiting; judged code may have left permission-locked entries.\n forciblyRemoveDirectorySync(liveRun.tempRoot);\n }\n process.kill(process.pid, signal);\n });\n }\n}\n\n// On Windows the harness is not detached (process groups are unavailable), so killing only the\n// direct child would leave submission grandchildren running; taskkill terminates the whole tree.\nfunction killHarnessTree(pid: number): void {\n if (process.platform === 'win32') {\n child_process.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });\n } else {\n process.kill(-pid, 'SIGKILL');\n }\n}\n\n// The all-problem check runs harnesses concurrently and has no sandbox-delegation contract, while the sandbox\n// helpers assume one harness at a time (their pkill targets every process of the sandbox user), so\n// never forward the sandbox user to judged runs.\nfunction createHarnessEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.EXERCODE_SANDBOX_USER;\n return env;\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":[],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\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';\n\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: ${error instanceof Error ? error.message : String(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 let nextRunIndex = 0;\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(options.concurrency, runs.length)) }, async () => {\n while (nextRunIndex < runs.length) {\n const run = runs[nextRunIndex++];\n if (!run) return;\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 );\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(\n `failed to copy the problem directory to a temporary location: ${error instanceof Error ? error.message : String(error)}`\n );\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: createHarnessEnv(),\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(\n `harness execution failed: ${error instanceof Error ? error.message : String(error)}`\n );\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\n// The all-problem check runs harnesses concurrently and has no sandbox-delegation contract, while the sandbox\n// helpers assume one harness at a time (their pkill targets every process of the sandbox user), so\n// never forward the sandbox user to judged runs.\nfunction createHarnessEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.EXERCODE_SANDBOX_USER;\n return env;\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,2 +1,2 @@
1
- "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../types/decisionCode.cjs"),r=require("./printDebugBanner.cjs");let i=require("node:path");i=e.__toESM(i,1);let a=require("node:child_process");a=e.__toESM(a,1);let o=require("node:fs");o=e.__toESM(o,1);let s=require("node:os");s=e.__toESM(s,1);async function c(e,t,n){let o;try{let s=await l(e);o=s.tempRoot;let{copiedProblemDir:c}=s,u=i.default.resolve(e),d=i.default.relative(u,i.default.resolve(t.cwd)),f=i.default.join(c,d),p=y(u);if(p.startsWith(`..`)||i.default.isAbsolute(p))return r.printDebugBanner([`[DEBUG MODE] isolated problem directory check skipped`,``,`The invoked judge script is located outside the problem directory.`,`Script path: ${p}`]),{passed:!0};let m=process.execArgv.filter(h),_=JSON.stringify(g(n)?n:{}),v=a.default.spawnSync(process.execPath,[...m,p,f,_],{cwd:c,encoding:`utf8`,env:process.env,timeout:3e4}),x=v.stdout??``,S=v.stderr??``;return v.status===0&&b(x)?(r.printDebugBanner([`[DEBUG MODE] isolated problem directory check passed`,``,`Copied problem dir : ${c}`,`Checked cwd : ${d}`]),{passed:!0}):(r.printDebugBanner([`[DEBUG MODE] isolated problem directory check failed`,``,`The judge did not complete successfully after copying only the problem directory to a temporary location.`,`Make sure judge.ts imports only files included in the problem directory.`,``,`Copied problem dir : ${c}`,`Checked cwd : ${d}`,`Exit status : ${v.status??v.signal??`unknown`}`,`Spawn error : ${v.error?.message??`<none>`}`,``,`stdout:`,x.trimEnd()||`<empty>`,``,`stderr:`,S.trimEnd()||`<empty>`]),{passed:!1})}catch(e){return r.printDebugBanner([`[DEBUG MODE] isolated problem directory check failed due to an unexpected error`,``,e instanceof Error?e.message:String(e)]),{passed:!1}}finally{o&&await u(o)}}async function l(e){let t=await o.default.promises.mkdtemp(i.default.join(s.default.tmpdir(),`problem-utils-isolation_`));try{let n=i.default.resolve(e),r=i.default.join(t,v(n));return await o.default.promises.mkdir(i.default.dirname(r),{recursive:!0}),await o.default.promises.cp(n,r,{recursive:!0,filter:p,verbatimSymlinks:!0}),await _(t,n),{tempRoot:t,copiedProblemDir:r}}catch(e){throw await u(t),e}}async function u(e){try{return await o.default.promises.rm(e,{recursive:!0,force:!0}),!0}catch{f(e);try{return await o.default.promises.rm(e,{recursive:!0,force:!0}),!0}catch{return!1}}}function d(e){try{return o.default.rmSync(e,{recursive:!0,force:!0}),!0}catch{f(e);try{return o.default.rmSync(e,{recursive:!0,force:!0}),!0}catch{return!1}}}function f(e){process.platform!==`win32`&&a.default.spawnSync(`chmod`,[`-R`,`u+rwX`,e])}async function p(e){if(!m(e))return!1;if((await o.default.promises.lstat(e)).isSymbolicLink()&&i.default.isAbsolute(await o.default.promises.readlink(e)))throw Error(`${e} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);return!0}function m(e){let t=i.default.basename(e);return t!==`node_modules`&&t!==`.git`}function h(e){return!e.startsWith(`--inspect`)&&!e.startsWith(`--watch`)&&!e.startsWith(`--hot`)}function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}async function _(e,t){let n=i.default.resolve(t);for(;;){let t=i.default.join(n,`node_modules`);if(o.default.existsSync(t)){let r=i.default.join(e,v(n),`node_modules`);try{await o.default.promises.symlink(t,r,process.platform===`win32`?`junction`:`dir`)}catch{}}let r=i.default.dirname(n);if(r===n)break;n=r}}function v(e){let{root:t}=i.default.parse(e);return i.default.relative(t,e)}function y(e){let t=process.argv[1];if(!t)return`.${i.default.sep}judge.ts`;let n=i.default.relative(e,i.default.resolve(t));return i.default.isAbsolute(n)||n.startsWith(`.`)?n:`.${i.default.sep}${n}`}function b(e){let r=e.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX));return r.length!==0&&r.every(e=>{try{let r=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)));return r.success&&r.data.decisionCode===n.DecisionCode.ACCEPTED}catch{return!1}})}exports.checkProblemDirIsolation=c,exports.copyProblemDirToTemporaryRoot=l,exports.forciblyRemoveDirectory=u,exports.forciblyRemoveDirectorySync=d;
1
+ "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../types/decisionCode.cjs"),r=require("./printDebugBanner.cjs"),i=require("./temporaryProblemDirCopy.cjs"),a=require("./runHarnessProcess.cjs");let o=require("node:path");o=e.__toESM(o,1);const s=3e4,c=1e4;async function l(e,t,n,l={}){let h=Math.max(s,(l.expectedMaxDurationMs??0)+c),g;try{let s=await i.copyProblemDirToTemporaryRoot(e);g=s.tempRoot;let{copiedProblemDir:c}=s,_=o.default.resolve(e),v=o.default.relative(_,o.default.resolve(t.cwd)),y=o.default.join(c,v),b=p(_);if(b.startsWith(`..`)||o.default.isAbsolute(b))return r.printDebugBanner([`[DEBUG MODE] isolated problem directory check skipped`,``,`The invoked judge script is located outside the problem directory.`,`Script path: ${b}`]),{passed:!0};let x=process.execArgv.filter(d),S=JSON.stringify(f(n)?n:{}),C=await a.runHarnessProcess([...x,b,y,S],{cwd:c,env:process.env,timeoutMs:h,maxOutputBytes:67108864,tempRoot:g});return C.exitCode===0&&m(C.stdout)?(r.printDebugBanner([`[DEBUG MODE] isolated problem directory check passed`,``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`]),{passed:!0}):(r.printDebugBanner([`[DEBUG MODE] isolated problem directory check failed`,``,...u(C,h,l),``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`,`Exit status : ${C.exitCode??C.signal??`unknown`}`,`Failure reason : ${C.failureReason??`<none>`}`,``,`stdout:`,C.stdout.trimEnd()||`<empty>`,``,`stderr:`,C.stderr.trimEnd()||`<empty>`]),{passed:!1})}catch(e){return r.printDebugBanner([`[DEBUG MODE] isolated problem directory check failed due to an unexpected error`,``,e instanceof Error?e.message:String(e)]),{passed:!1}}finally{g&&await i.forciblyRemoveDirectory(g)}}function u(e,t,n){return e.timedOut?[`The judge did not finish within ${t/1e3} seconds after copying only the problem directory to a temporary location.`,...n.expectedMaxDurationMs===void 0?[`This judge declares no run time, so the check uses its fixed ${s/1e3}-second budget.`]:[`The budget is the larger of ${s/1e3} seconds and the build timeout plus the time limit of every test case plus a ${c/1e3}-second overhead.`,`Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.`]]:e.failureReason===void 0?e.exitCode===void 0?[`The judge was terminated by ${e.signal??`an unknown signal`} (e.g. an out-of-memory kill or a cancelled job).`]:[`The judge did not complete successfully after copying only the problem directory to a temporary location.`,`Make sure judge.ts imports only files included in the problem directory.`]:[`The judge was stopped before it finished; see the failure reason below.`]}function d(e){return!e.startsWith(`--inspect`)&&!e.startsWith(`--watch`)&&!e.startsWith(`--hot`)}function f(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function p(e){let t=process.argv[1];if(!t)return`.${o.default.sep}judge.ts`;let n=o.default.relative(e,o.default.resolve(t));return o.default.isAbsolute(n)||n.startsWith(`.`)?n:`.${o.default.sep}${n}`}function m(e){let r=e.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX));return r.length!==0&&r.every(e=>{try{let r=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)));return r.success&&r.data.decisionCode===n.DecisionCode.ACCEPTED}catch{return!1}})}exports.checkProblemDirIsolation=l;
2
2
  //# sourceMappingURL=checkProblemDirIsolation.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkProblemDirIsolation.cjs","names":["path","child_process","fs","os","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","DecisionCode"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\n\nconst ISOLATION_CHECK_TIMEOUT_MS = 30_000;\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): Promise<ProblemDirIsolationCheckResult> {\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(isJudgeParamsObject(params) ? params : {});\n const spawnResult = child_process.spawnSync(process.execPath, [...execArgv, scriptPath, copiedCwd, paramsJson], {\n cwd: copiedProblemDir,\n encoding: 'utf8',\n env: process.env,\n timeout: ISOLATION_CHECK_TIMEOUT_MS,\n });\n const stdout = spawnResult.stdout ?? '';\n const stderr = spawnResult.stderr ?? '';\n\n if (spawnResult.status === 0 && isAcceptedJudgeOutput(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 '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 `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n `Exit status : ${spawnResult.status ?? spawnResult.signal ?? 'unknown'}`,\n `Spawn error : ${spawnResult.error?.message ?? '<none>'}`,\n '',\n 'stdout:',\n stdout.trimEnd() || '<empty>',\n '',\n 'stderr:',\n 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 error instanceof Error ? error.message : String(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\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n problemDir: string\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n try {\n const absoluteProblemDir = path.resolve(problemDir);\n const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n recursive: true,\n filter: rejectAbsoluteSymlinks,\n // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n // source tree, so judging the copy could write through them into the checked repository.\n verbatimSymlinks: true,\n });\n await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n return { tempRoot, copiedProblemDir };\n } catch (error) {\n await forciblyRemoveDirectory(tempRoot);\n throw error;\n }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n if (!isCopiedProblemPath(src)) return false;\n const stats = await fs.promises.lstat(src);\n if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n }\n return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n const name = path.basename(src);\n return name !== 'node_modules' && name !== '.git';\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction isJudgeParamsObject(params: unknown): params is object {\n return params !== undefined && params !== null && typeof params === 'object' && !Array.isArray(params);\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n let currentDir = path.resolve(problemDir);\n while (true) {\n const nodeModulesPath = path.join(currentDir, 'node_modules');\n if (fs.existsSync(nodeModulesPath)) {\n const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n try {\n await fs.promises.symlink(\n nodeModulesPath,\n targetSymlinkPath,\n process.platform === 'win32' ? 'junction' : 'dir'\n );\n } catch {\n // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n const { root } = path.parse(absolutePath);\n return path.relative(root, absolutePath);\n}\n\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":"2WAoBA,eAAsB,EACpB,EACA,EACA,EACyC,CACzC,IAAI,EACJ,GAAI,CACF,IAAM,EAAa,MAAM,EAA8B,CAAU,EACjE,EAAW,EAAW,SACtB,GAAM,CAAE,oBAAqB,EACvB,EAAqBA,EAAAA,QAAK,QAAQ,CAAU,EAE5C,EAAcA,EAAAA,QAAK,SAAS,EAAoBA,EAAAA,QAAK,QAAQ,EAAY,GAAG,CAAC,EAC7E,EAAYA,EAAAA,QAAK,KAAK,EAAkB,CAAW,EACnD,EAAa,EAAqB,CAAkB,EAC1D,GAAI,EAAW,WAAW,IAAI,GAAKA,EAAAA,QAAK,WAAW,CAAU,EAO3D,OANA,EAAA,iBAAiB,CACf,wDACA,GACA,qEACA,gBAAgB,GAClB,CAAC,EACM,CAAE,OAAQ,EAAK,EAExB,IAAM,EAAW,QAAQ,SAAS,OAAO,CAAkB,EACrD,EAAa,KAAK,UAAU,EAAoB,CAAM,EAAI,EAAS,CAAC,CAAC,EACrE,EAAcC,EAAAA,QAAc,UAAU,QAAQ,SAAU,CAAC,GAAG,EAAU,EAAY,EAAW,CAAU,EAAG,CAC9G,IAAK,EACL,SAAU,OACV,IAAK,QAAQ,IACb,QAAS,GACX,CAAC,EACK,EAAS,EAAY,QAAU,GAC/B,EAAS,EAAY,QAAU,GA6BrC,OA3BI,EAAY,SAAW,GAAK,EAAsB,CAAM,GAC1D,EAAA,iBAAiB,CACf,uDACA,GACA,wBAAwB,IACxB,wBAAwB,GAC1B,CAAC,EACM,CAAE,OAAQ,EAAK,IAGxB,EAAA,iBAAiB,CACf,uDACA,GACA,4GACA,2EACA,GACA,wBAAwB,IACxB,wBAAwB,IACxB,wBAAwB,EAAY,QAAU,EAAY,QAAU,YACpE,wBAAwB,EAAY,OAAO,SAAW,WACtD,GACA,UACA,EAAO,QAAQ,GAAK,UACpB,GACA,UACA,EAAO,QAAQ,GAAK,SACtB,CAAC,EACM,CAAE,OAAQ,EAAM,EACzB,OAAS,EAAO,CAMd,OALA,EAAA,iBAAiB,CACf,kFACA,GACA,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CACvD,CAAC,EACM,CAAE,OAAQ,EAAM,CACzB,QAAU,CAEJ,GAAU,MAAM,EAAwB,CAAQ,CACtD,CACF,CAMA,eAAsB,EACpB,EACyD,CACzD,IAAM,EAAW,MAAMC,EAAAA,QAAG,SAAS,QAAQF,EAAAA,QAAK,KAAKG,EAAAA,QAAG,OAAO,EAAG,0BAA0B,CAAC,EAC7F,GAAI,CACF,IAAM,EAAqBH,EAAAA,QAAK,QAAQ,CAAU,EAC5C,EAAmBA,EAAAA,QAAK,KAAK,EAAU,EAAmB,CAAkB,CAAC,EAUnF,OATA,MAAME,EAAAA,QAAG,SAAS,MAAMF,EAAAA,QAAK,QAAQ,CAAgB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3E,MAAME,EAAAA,QAAG,SAAS,GAAG,EAAoB,EAAkB,CACzD,UAAW,GACX,OAAQ,EAGR,iBAAkB,EACpB,CAAC,EACD,MAAM,EAA8B,EAAU,CAAkB,EACzD,CAAE,WAAU,kBAAiB,CACtC,OAAS,EAAO,CAEd,MADA,MAAM,EAAwB,CAAQ,EAChC,CACR,CACF,CAMA,eAAsB,EAAwB,EAA+B,CAC3E,GAAI,CAEF,OADA,MAAMA,EAAAA,QAAG,SAAS,GAAG,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,EAAkB,CAAG,EACrB,GAAI,CAEF,OADA,MAAMA,EAAAA,QAAG,SAAS,GAAG,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAGA,SAAgB,EAA4B,EAAsB,CAChE,GAAI,CAEF,OADA,EAAA,QAAG,OAAO,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,EAAkB,CAAG,EACrB,GAAI,CAEF,OADA,EAAA,QAAG,OAAO,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAIA,SAAS,EAAkB,EAAmB,CACxC,QAAQ,WAAa,SAAS,EAAA,QAAc,UAAU,QAAS,CAAC,KAAM,QAAS,CAAG,CAAC,CACzF,CAIA,eAAe,EAAuB,EAA+B,CACnE,GAAI,CAAC,EAAoB,CAAG,EAAG,MAAO,GAEtC,IAAI,MADgBA,EAAAA,QAAG,SAAS,MAAM,CAAG,EAAA,CAC/B,eAAe,GAAKF,EAAAA,QAAK,WAAW,MAAME,EAAAA,QAAG,SAAS,SAAS,CAAG,CAAC,EAC3E,MAAU,MAAM,GAAG,EAAI,uFAAuF,EAEhH,MAAO,EACT,CAEA,SAAS,EAAoB,EAAsB,CACjD,IAAM,EAAOF,EAAAA,QAAK,SAAS,CAAG,EAC9B,OAAO,IAAS,gBAAkB,IAAS,MAC7C,CAEA,SAAS,EAAmB,EAAsB,CAChD,MAAO,CAAC,EAAI,WAAW,WAAW,GAAK,CAAC,EAAI,WAAW,SAAS,GAAK,CAAC,EAAI,WAAW,OAAO,CAC9F,CAEA,SAAS,EAAoB,EAAmC,CAC9D,OAAkD,OAAO,GAAW,YAA7D,GAAyE,CAAC,MAAM,QAAQ,CAAM,CACvG,CAEA,eAAe,EAA8B,EAAkB,EAAmC,CAChG,IAAI,EAAaA,EAAAA,QAAK,QAAQ,CAAU,EACxC,OAAa,CACX,IAAM,EAAkBA,EAAAA,QAAK,KAAK,EAAY,cAAc,EAC5D,GAAIE,EAAAA,QAAG,WAAW,CAAe,EAAG,CAClC,IAAM,EAAoBF,EAAAA,QAAK,KAAK,EAAU,EAAmB,CAAU,EAAG,cAAc,EAC5F,GAAI,CACF,MAAME,EAAAA,QAAG,SAAS,QAChB,EACA,EACA,QAAQ,WAAa,QAAU,WAAa,KAC9C,CACF,MAAQ,CAER,CACF,CAEA,IAAM,EAAYF,EAAAA,QAAK,QAAQ,CAAU,EACzC,GAAI,IAAc,EAAY,MAC9B,EAAa,CACf,CACF,CAEA,SAAS,EAAmB,EAA8B,CACxD,GAAM,CAAE,QAASA,EAAAA,QAAK,MAAM,CAAY,EACxC,OAAOA,EAAAA,QAAK,SAAS,EAAM,CAAY,CACzC,CAEA,SAAS,EAAqB,EAA4B,CACxD,IAAM,EAAa,QAAQ,KAAK,GAChC,GAAI,CAAC,EAAY,MAAO,IAAIA,EAAAA,QAAK,IAAI,UACrC,IAAM,EAAqBA,EAAAA,QAAK,SAAS,EAAYA,EAAAA,QAAK,QAAQ,CAAU,CAAC,EAE7E,OADIA,EAAAA,QAAK,WAAW,CAAkB,GAC/B,EAAmB,WAAW,GAAG,EADQ,EACiB,IAAIA,EAAAA,QAAK,MAAM,GAClF,CAEA,SAAS,EAAsB,EAAyB,CACtD,IAAM,EAAc,EAAO,MAAM,OAAO,CAAC,CAAC,OAAQ,GAAS,EAAK,WAAWI,EAAAA,uBAAuB,CAAC,EAGnG,OAFI,EAAY,SAAW,GAEpB,EAAY,MAAO,GAAS,CACjC,GAAI,CACF,IAAM,EAAeC,EAAAA,qBAAqB,UAAU,KAAK,MAAM,EAAK,MAAMD,EAAAA,wBAAwB,MAAM,CAAC,CAAC,EAC1G,OAAO,EAAa,SAAW,EAAa,KAAK,eAAiBE,EAAAA,aAAa,QACjF,MAAQ,CACN,MAAO,EACT,CACF,CAAC,CACH"}
1
+ {"version":3,"file":"checkProblemDirIsolation.cjs","names":["copyProblemDirToTemporaryRoot","path","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","DecisionCode"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\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(isJudgeParamsObject(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 error instanceof Error ? error.message : String(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 isJudgeParamsObject(params: unknown): params is object {\n return params !== undefined && params !== null && typeof params === 'object' && !Array.isArray(params);\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,23 +1,16 @@
1
1
  import type { ResolvedCwd } from './resolveCwds.js';
2
+ export interface ProblemDirIsolationCheckOptions {
3
+ /**
4
+ * The judge's declared worst-case duration for one accepted submission (build timeout plus the
5
+ * time limit of every test case). The check allows at least that plus a fixed overhead; per-case
6
+ * process startup and custom judge callbacks are not budgeted separately.
7
+ */
8
+ expectedMaxDurationMs?: number;
9
+ }
2
10
  export interface ProblemDirIsolationCheckResult {
3
11
  passed: boolean;
4
12
  }
5
13
  /**
6
14
  * Check that a judge can run after only the problem directory is copied elsewhere.
7
15
  */
8
- export declare function checkProblemDirIsolation(problemDir: string, resolvedCwd: ResolvedCwd, params: unknown): Promise<ProblemDirIsolationCheckResult>;
9
- /**
10
- * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the
11
- * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.
12
- */
13
- export declare function copyProblemDirToTemporaryRoot(problemDir: string): Promise<{
14
- tempRoot: string;
15
- copiedProblemDir: string;
16
- }>;
17
- /**
18
- * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a
19
- * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.
20
- */
21
- export declare function forciblyRemoveDirectory(dir: string): Promise<boolean>;
22
- /** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */
23
- export declare function forciblyRemoveDirectorySync(dir: string): boolean;
16
+ export declare function checkProblemDirIsolation(problemDir: string, resolvedCwd: ResolvedCwd, params: unknown, options?: ProblemDirIsolationCheckOptions): Promise<ProblemDirIsolationCheckResult>;
@@ -1,2 +1,2 @@
1
- import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{DecisionCode as n}from"../types/decisionCode.js";import{printDebugBanner as r}from"./printDebugBanner.js";import i from"node:path";import a from"node:child_process";import o from"node:fs";import s from"node:os";async function c(e,t,n){let o;try{let s=await l(e);o=s.tempRoot;let{copiedProblemDir:c}=s,u=i.resolve(e),d=i.relative(u,i.resolve(t.cwd)),f=i.join(c,d),p=y(u);if(p.startsWith(`..`)||i.isAbsolute(p))return r([`[DEBUG MODE] isolated problem directory check skipped`,``,`The invoked judge script is located outside the problem directory.`,`Script path: ${p}`]),{passed:!0};let m=process.execArgv.filter(h),_=JSON.stringify(g(n)?n:{}),v=a.spawnSync(process.execPath,[...m,p,f,_],{cwd:c,encoding:`utf8`,env:process.env,timeout:3e4}),x=v.stdout??``,S=v.stderr??``;return v.status===0&&b(x)?(r([`[DEBUG MODE] isolated problem directory check passed`,``,`Copied problem dir : ${c}`,`Checked cwd : ${d}`]),{passed:!0}):(r([`[DEBUG MODE] isolated problem directory check failed`,``,`The judge did not complete successfully after copying only the problem directory to a temporary location.`,`Make sure judge.ts imports only files included in the problem directory.`,``,`Copied problem dir : ${c}`,`Checked cwd : ${d}`,`Exit status : ${v.status??v.signal??`unknown`}`,`Spawn error : ${v.error?.message??`<none>`}`,``,`stdout:`,x.trimEnd()||`<empty>`,``,`stderr:`,S.trimEnd()||`<empty>`]),{passed:!1})}catch(e){return r([`[DEBUG MODE] isolated problem directory check failed due to an unexpected error`,``,e instanceof Error?e.message:String(e)]),{passed:!1}}finally{o&&await u(o)}}async function l(e){let t=await o.promises.mkdtemp(i.join(s.tmpdir(),`problem-utils-isolation_`));try{let n=i.resolve(e),r=i.join(t,v(n));return await o.promises.mkdir(i.dirname(r),{recursive:!0}),await o.promises.cp(n,r,{recursive:!0,filter:p,verbatimSymlinks:!0}),await _(t,n),{tempRoot:t,copiedProblemDir:r}}catch(e){throw await u(t),e}}async function u(e){try{return await o.promises.rm(e,{recursive:!0,force:!0}),!0}catch{f(e);try{return await o.promises.rm(e,{recursive:!0,force:!0}),!0}catch{return!1}}}function d(e){try{return o.rmSync(e,{recursive:!0,force:!0}),!0}catch{f(e);try{return o.rmSync(e,{recursive:!0,force:!0}),!0}catch{return!1}}}function f(e){process.platform!==`win32`&&a.spawnSync(`chmod`,[`-R`,`u+rwX`,e])}async function p(e){if(!m(e))return!1;if((await o.promises.lstat(e)).isSymbolicLink()&&i.isAbsolute(await o.promises.readlink(e)))throw Error(`${e} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);return!0}function m(e){let t=i.basename(e);return t!==`node_modules`&&t!==`.git`}function h(e){return!e.startsWith(`--inspect`)&&!e.startsWith(`--watch`)&&!e.startsWith(`--hot`)}function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}async function _(e,t){let n=i.resolve(t);for(;;){let t=i.join(n,`node_modules`);if(o.existsSync(t)){let r=i.join(e,v(n),`node_modules`);try{await o.promises.symlink(t,r,process.platform===`win32`?`junction`:`dir`)}catch{}}let r=i.dirname(n);if(r===n)break;n=r}}function v(e){let{root:t}=i.parse(e);return i.relative(t,e)}function y(e){let t=process.argv[1];if(!t)return`.${i.sep}judge.ts`;let n=i.relative(e,i.resolve(t));return i.isAbsolute(n)||n.startsWith(`.`)?n:`.${i.sep}${n}`}function b(r){let i=r.split(/\r?\n/).filter(t=>t.startsWith(e));return i.length!==0&&i.every(r=>{try{let i=t.safeParse(JSON.parse(r.slice(e.length)));return i.success&&i.data.decisionCode===n.ACCEPTED}catch{return!1}})}export{c as checkProblemDirIsolation,l as copyProblemDirToTemporaryRoot,u as forciblyRemoveDirectory,d as forciblyRemoveDirectorySync};
1
+ import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{DecisionCode as n}from"../types/decisionCode.js";import{printDebugBanner as r}from"./printDebugBanner.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"./temporaryProblemDirCopy.js";import{runHarnessProcess as o}from"./runHarnessProcess.js";import s from"node:path";const c=3e4,l=1e4;async function u(e,t,n,u={}){let g=Math.max(c,(u.expectedMaxDurationMs??0)+l),_;try{let a=await i(e);_=a.tempRoot;let{copiedProblemDir:c}=a,l=s.resolve(e),v=s.relative(l,s.resolve(t.cwd)),y=s.join(c,v),b=m(l);if(b.startsWith(`..`)||s.isAbsolute(b))return r([`[DEBUG MODE] isolated problem directory check skipped`,``,`The invoked judge script is located outside the problem directory.`,`Script path: ${b}`]),{passed:!0};let x=process.execArgv.filter(f),S=JSON.stringify(p(n)?n:{}),C=await o([...x,b,y,S],{cwd:c,env:process.env,timeoutMs:g,maxOutputBytes:67108864,tempRoot:_});return C.exitCode===0&&h(C.stdout)?(r([`[DEBUG MODE] isolated problem directory check passed`,``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`]),{passed:!0}):(r([`[DEBUG MODE] isolated problem directory check failed`,``,...d(C,g,u),``,`Copied problem dir : ${c}`,`Checked cwd : ${v}`,`Exit status : ${C.exitCode??C.signal??`unknown`}`,`Failure reason : ${C.failureReason??`<none>`}`,``,`stdout:`,C.stdout.trimEnd()||`<empty>`,``,`stderr:`,C.stderr.trimEnd()||`<empty>`]),{passed:!1})}catch(e){return r([`[DEBUG MODE] isolated problem directory check failed due to an unexpected error`,``,e instanceof Error?e.message:String(e)]),{passed:!1}}finally{_&&await a(_)}}function d(e,t,n){return e.timedOut?[`The judge did not finish within ${t/1e3} seconds after copying only the problem directory to a temporary location.`,...n.expectedMaxDurationMs===void 0?[`This judge declares no run time, so the check uses its fixed ${c/1e3}-second budget.`]:[`The budget is the larger of ${c/1e3} seconds and the build timeout plus the time limit of every test case plus a ${l/1e3}-second overhead.`,`Check timeLimitMs, the number of test cases, and one-off startup costs such as cold caches.`]]:e.failureReason===void 0?e.exitCode===void 0?[`The judge was terminated by ${e.signal??`an unknown signal`} (e.g. an out-of-memory kill or a cancelled job).`]:[`The judge did not complete successfully after copying only the problem directory to a temporary location.`,`Make sure judge.ts imports only files included in the problem directory.`]:[`The judge was stopped before it finished; see the failure reason below.`]}function f(e){return!e.startsWith(`--inspect`)&&!e.startsWith(`--watch`)&&!e.startsWith(`--hot`)}function p(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function m(e){let t=process.argv[1];if(!t)return`.${s.sep}judge.ts`;let n=s.relative(e,s.resolve(t));return s.isAbsolute(n)||n.startsWith(`.`)?n:`.${s.sep}${n}`}function h(r){let i=r.split(/\r?\n/).filter(t=>t.startsWith(e));return i.length!==0&&i.every(r=>{try{let i=t.safeParse(JSON.parse(r.slice(e.length)));return i.success&&i.data.decisionCode===n.ACCEPTED}catch{return!1}})}export{u as checkProblemDirIsolation};
2
2
  //# sourceMappingURL=checkProblemDirIsolation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkProblemDirIsolation.js","names":[],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\n\nconst ISOLATION_CHECK_TIMEOUT_MS = 30_000;\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): Promise<ProblemDirIsolationCheckResult> {\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(isJudgeParamsObject(params) ? params : {});\n const spawnResult = child_process.spawnSync(process.execPath, [...execArgv, scriptPath, copiedCwd, paramsJson], {\n cwd: copiedProblemDir,\n encoding: 'utf8',\n env: process.env,\n timeout: ISOLATION_CHECK_TIMEOUT_MS,\n });\n const stdout = spawnResult.stdout ?? '';\n const stderr = spawnResult.stderr ?? '';\n\n if (spawnResult.status === 0 && isAcceptedJudgeOutput(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 '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 `Copied problem dir : ${copiedProblemDir}`,\n `Checked cwd : ${relativeCwd}`,\n `Exit status : ${spawnResult.status ?? spawnResult.signal ?? 'unknown'}`,\n `Spawn error : ${spawnResult.error?.message ?? '<none>'}`,\n '',\n 'stdout:',\n stdout.trimEnd() || '<empty>',\n '',\n 'stderr:',\n 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 error instanceof Error ? error.message : String(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\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n problemDir: string\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n try {\n const absoluteProblemDir = path.resolve(problemDir);\n const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n recursive: true,\n filter: rejectAbsoluteSymlinks,\n // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n // source tree, so judging the copy could write through them into the checked repository.\n verbatimSymlinks: true,\n });\n await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n return { tempRoot, copiedProblemDir };\n } catch (error) {\n await forciblyRemoveDirectory(tempRoot);\n throw error;\n }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n await fs.promises.rm(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n unlockPermissions(dir);\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n if (!isCopiedProblemPath(src)) return false;\n const stats = await fs.promises.lstat(src);\n if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n }\n return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n const name = path.basename(src);\n return name !== 'node_modules' && name !== '.git';\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction isJudgeParamsObject(params: unknown): params is object {\n return params !== undefined && params !== null && typeof params === 'object' && !Array.isArray(params);\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n let currentDir = path.resolve(problemDir);\n while (true) {\n const nodeModulesPath = path.join(currentDir, 'node_modules');\n if (fs.existsSync(nodeModulesPath)) {\n const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n try {\n await fs.promises.symlink(\n nodeModulesPath,\n targetSymlinkPath,\n process.platform === 'win32' ? 'junction' : 'dir'\n );\n } catch {\n // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n const { root } = path.parse(absolutePath);\n return path.relative(root, absolutePath);\n}\n\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":[],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\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(isJudgeParamsObject(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 error instanceof Error ? error.message : String(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 isJudgeParamsObject(params: unknown): params is object {\n return params !== undefined && params !== null && typeof params === 'object' && !Array.isArray(params);\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,19 +1,18 @@
1
- "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./removeCommentsInSourceCode.cjs"),n=require("../types/decisionCode.cjs"),r=require("./findLanguageDefinitionByPath.cjs");let i=require("node:path");i=e.__toESM(i,1);let a=require("node:fs");a=e.__toESM(a,1);async function o(e,o){let s=new Set,c=[];for(let n of await a.default.promises.readdir(e,{withFileTypes:!0,recursive:!0})){if(!n.isFile())continue;let o=i.default.relative(e,i.default.join(n.parentPath,n.name));s.add(o);let l=await a.default.promises.readFile(i.default.join(n.parentPath,n.name),`utf8`);if(l.includes(`�`))continue;let u=r.findLanguageDefinitionByPath(n.name);u&&c.push({path:n.name,data:u.grammer?t.removeCommentsInSourceCode(u.grammer,l):l})}if(o.requiredSubmissionFilePaths){let e=o.requiredSubmissionFilePaths.filter(e=>!s.has(e)).toSorted();if(e.length>0)return{decisionCode:n.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`ファイルが不足しています。
1
+ "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./removeCommentsInSourceCode.cjs"),n=require("../types/decisionCode.cjs"),r=require("../types/problem.cjs"),i=require("./findLanguageDefinitionByPath.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs");o=e.__toESM(o,1);async function s(e,s){let l=new Set,u=[];for(let n of await o.default.promises.readdir(e,{withFileTypes:!0,recursive:!0})){if(!n.isFile())continue;let r=a.default.relative(e,a.default.join(n.parentPath,n.name));l.add(r);let s=await o.default.promises.readFile(a.default.join(n.parentPath,n.name),`utf8`);if(s.includes(`�`))continue;let c=i.findLanguageDefinitionByPath(n.name);c&&u.push({path:r,data:c.grammer?t.removeCommentsInSourceCode(c.grammer,s):s})}if(s.requiredSubmissionFilePaths){let e=s.requiredSubmissionFilePaths.filter(e=>!l.has(e)).toSorted();if(e.length>0)return{decisionCode:n.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`ファイルが不足しています。
2
2
  次のファイルを追加してから再度提出してください。
3
3
 
4
4
  ${e.map(e=>`- \`${e}\``).join(`
5
5
  `)}
6
- `}}let l=[];for(let e of c){for(let t of o.forbiddenRegExpsInCode??[]){let n=new RegExp(t,`g`),r=e.data.matchAll(n);for(let t of r)l.push({pattern:n.toString(),path:e.path,match:t[0]})}for(let t of o.forbiddenTextsInCode??[]){let n=0;for(;n<e.data.length;){let r=e.data.indexOf(t,n);if(r===-1)break;l.push({pattern:t,path:e.path,match:t}),n=r+t.length}}}if(l.length>0)return{decisionCode:n.DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
6
+ `}}let d=[];for(let e of s.forbiddenRegExpsInCode??[]){let{pattern:t,message:n}=r.normalizeCodeRule(e),i=new RegExp(t,`g`),a=u.flatMap(e=>[...e.data.matchAll(i)].map(t=>({path:e.path,match:t[0]})));a.length>0&&d.push({label:n??`禁止パターン \`${i.toString()}\``,matches:a})}for(let e of s.forbiddenTextsInCode??[]){let{pattern:t,message:n}=r.normalizeCodeRule(e),i=u.filter(e=>e.data.includes(t)).map(e=>({path:e.path,match:t}));i.length>0&&d.push({label:n??`禁止文字列 \`${t}\``,matches:i})}if(d.length>0)return{decisionCode:n.DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
7
7
  ソースコードを修正してから再度提出してください。
8
8
 
9
- | ファイル | 禁止パターン | 文字列 |
10
- | -------- | ------------ | ------ |
11
- ${l.map(e=>`| \`${e.path}\` | \`${e.pattern}\` | \`${e.match}\` |`).join(`
9
+ ${d.map(c).join(`
12
10
  `)}
13
- `};let u=[];for(let e of o.requiredRegExpsInCode??[]){let t=new RegExp(e);c.some(e=>t.test(e.data))||u.push(t.toString())}if(u.length>0)return{decisionCode:n.DecisionCode.REQUIRED_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に必要な文字列が含まれていません。
11
+ `};let f=[];for(let e of s.requiredRegExpsInCode??[]){let{pattern:t,message:n}=r.normalizeCodeRule(e),i=new RegExp(t);u.some(e=>i.test(e.data))||f.push(n??`\`${i.toString()}\``)}if(f.length>0)return{decisionCode:n.DecisionCode.REQUIRED_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に必要な文字列が含まれていません。
14
12
  ソースコードを修正してから再度提出してください。
15
13
 
16
- ${u.map(e=>`- \`${e}\``).join(`
14
+ ${f.map(e=>`- ${e}`).join(`
17
15
  `)}
18
- `}}exports.judgeByStaticAnalysis=o;
16
+ `}}function c(e){let t=[...new Set(e.matches.map(({path:e,match:t})=>` - \`${e}\`: \`${t}\``))];return`- ${e.label}\n${t.join(`
17
+ `)}`}exports.judgeByStaticAnalysis=s;
19
18
  //# sourceMappingURL=judgeByStaticAnalysis.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"judgeByStaticAnalysis.cjs","names":["fs","path","findLanguageDefinitionByPath","removeCommentsInSourceCode","DecisionCode"],"sources":["../../src/helpers/judgeByStaticAnalysis.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { findLanguageDefinitionByPath } from './findLanguageDefinitionByPath.js';\nimport { removeCommentsInSourceCode } from './removeCommentsInSourceCode.js';\n\nexport async function judgeByStaticAnalysis(\n cwd: string,\n problemMarkdownFrontMatterLike: Pick<\n ProblemMarkdownFrontMatter,\n 'forbiddenRegExpsInCode' | 'forbiddenTextsInCode' | 'requiredRegExpsInCode' | 'requiredSubmissionFilePaths'\n >\n): Promise<Pick<TestCaseResult, 'decisionCode' | 'feedbackMarkdown'> | undefined> {\n const filePathSet = new Set<string>();\n const sourceCodeWithoutCommentFiles: { path: string; data: string }[] = [];\n\n for (const dirent of await fs.promises.readdir(cwd, { withFileTypes: true, recursive: true })) {\n if (!dirent.isFile()) continue;\n\n const relativePath = path.relative(cwd, path.join(dirent.parentPath, dirent.name));\n filePathSet.add(relativePath);\n\n const text = await fs.promises.readFile(path.join(dirent.parentPath, dirent.name), 'utf8');\n const isBinary = text.includes('\\uFFFD');\n if (isBinary) continue;\n\n const languageDefinition = findLanguageDefinitionByPath(dirent.name);\n if (!languageDefinition) continue;\n\n sourceCodeWithoutCommentFiles.push({\n path: dirent.name,\n data: languageDefinition.grammer ? removeCommentsInSourceCode(languageDefinition.grammer, text) : text,\n });\n }\n\n if (problemMarkdownFrontMatterLike.requiredSubmissionFilePaths) {\n const missingFilePaths = problemMarkdownFrontMatterLike.requiredSubmissionFilePaths\n .filter((p) => !filePathSet.has(p))\n .toSorted();\n\n if (missingFilePaths.length > 0) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n feedbackMarkdown: `ファイルが不足しています。\n次のファイルを追加してから再度提出してください。\n\n${missingFilePaths.map((p) => `- \\`${p}\\``).join('\\n')}\n`,\n };\n }\n }\n\n const forbiddenFounds: { pattern: string; path: string; match: string }[] = [];\n\n for (const file of sourceCodeWithoutCommentFiles) {\n for (const pattern of problemMarkdownFrontMatterLike.forbiddenRegExpsInCode ?? []) {\n const re = new RegExp(pattern, 'g');\n const mathces = file.data.matchAll(re);\n for (const match of mathces) forbiddenFounds.push({ pattern: re.toString(), path: file.path, match: match[0] });\n }\n for (const pattern of problemMarkdownFrontMatterLike.forbiddenTextsInCode ?? []) {\n let p = 0;\n while (p < file.data.length) {\n const index = file.data.indexOf(pattern, p);\n if (index === -1) break;\n forbiddenFounds.push({ pattern, path: file.path, match: pattern });\n p = index + pattern.length;\n }\n }\n }\n\n if (forbiddenFounds.length > 0) {\n return {\n decisionCode: DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,\n feedbackMarkdown: `ソースコード中に禁止された文字列が含まれています。\nソースコードを修正してから再度提出してください。\n\n| ファイル | 禁止パターン | 文字列 |\n| -------- | ------------ | ------ |\n${forbiddenFounds.map((f) => `| \\`${f.path}\\` | \\`${f.pattern}\\` | \\`${f.match}\\` |`).join('\\n')}\n`,\n };\n }\n\n const missingRequiredPatterns: string[] = [];\n\n for (const pattern of problemMarkdownFrontMatterLike.requiredRegExpsInCode ?? []) {\n const re = new RegExp(pattern);\n const isFound = sourceCodeWithoutCommentFiles.some((f) => re.test(f.data));\n if (!isFound) missingRequiredPatterns.push(re.toString());\n }\n\n if (missingRequiredPatterns.length > 0) {\n return {\n decisionCode: DecisionCode.REQUIRED_PATTERNS_IN_CODE_ERROR,\n feedbackMarkdown: `ソースコード中に必要な文字列が含まれていません。\nソースコードを修正してから再度提出してください。\n\n${missingRequiredPatterns.map((p) => `- \\`${p}\\``).join('\\n')}\n`,\n };\n }\n\n return;\n}\n"],"mappings":"6RAUA,eAAsB,EACpB,EACA,EAIgF,CAChF,IAAM,EAAc,IAAI,IAClB,EAAkE,CAAC,EAEzE,IAAK,IAAM,KAAU,MAAMA,EAAAA,QAAG,SAAS,QAAQ,EAAK,CAAE,cAAe,GAAM,UAAW,EAAK,CAAC,EAAG,CAC7F,GAAI,CAAC,EAAO,OAAO,EAAG,SAEtB,IAAM,EAAeC,EAAAA,QAAK,SAAS,EAAKA,EAAAA,QAAK,KAAK,EAAO,WAAY,EAAO,IAAI,CAAC,EACjF,EAAY,IAAI,CAAY,EAE5B,IAAM,EAAO,MAAMD,EAAAA,QAAG,SAAS,SAASC,EAAAA,QAAK,KAAK,EAAO,WAAY,EAAO,IAAI,EAAG,MAAM,EAEzF,GADiB,EAAK,SAAS,GACpB,EAAG,SAEd,IAAM,EAAqBC,EAAAA,6BAA6B,EAAO,IAAI,EAC9D,GAEL,EAA8B,KAAK,CACjC,KAAM,EAAO,KACb,KAAM,EAAmB,QAAUC,EAAAA,2BAA2B,EAAmB,QAAS,CAAI,EAAI,CACpG,CAAC,CACH,CAEA,GAAI,EAA+B,4BAA6B,CAC9D,IAAM,EAAmB,EAA+B,4BACrD,OAAQ,GAAM,CAAC,EAAY,IAAI,CAAC,CAAC,CAAC,CAClC,SAAS,EAEZ,GAAI,EAAiB,OAAS,EAC5B,MAAO,CACL,aAAcC,EAAAA,aAAa,uCAC3B,iBAAkB;;;EAGxB,EAAiB,IAAK,GAAM,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK;CAAI,EAAE;CAEjD,CAEJ,CAEA,IAAM,EAAsE,CAAC,EAE7E,IAAK,IAAM,KAAQ,EAA+B,CAChD,IAAK,IAAM,KAAW,EAA+B,wBAA0B,CAAC,EAAG,CACjF,IAAM,EAAK,IAAI,OAAO,EAAS,GAAG,EAC5B,EAAU,EAAK,KAAK,SAAS,CAAE,EACrC,IAAK,IAAM,KAAS,EAAS,EAAgB,KAAK,CAAE,QAAS,EAAG,SAAS,EAAG,KAAM,EAAK,KAAM,MAAO,EAAM,EAAG,CAAC,CAChH,CACA,IAAK,IAAM,KAAW,EAA+B,sBAAwB,CAAC,EAAG,CAC/E,IAAI,EAAI,EACR,KAAO,EAAI,EAAK,KAAK,QAAQ,CAC3B,IAAM,EAAQ,EAAK,KAAK,QAAQ,EAAS,CAAC,EAC1C,GAAI,IAAU,GAAI,MAClB,EAAgB,KAAK,CAAE,UAAS,KAAM,EAAK,KAAM,MAAO,CAAQ,CAAC,EACjE,EAAI,EAAQ,EAAQ,MACtB,CACF,CACF,CAEA,GAAI,EAAgB,OAAS,EAC3B,MAAO,CACL,aAAcA,EAAAA,aAAa,iCAC3B,iBAAkB;;;;;EAKtB,EAAgB,IAAK,GAAM,OAAO,EAAE,KAAK,SAAS,EAAE,QAAQ,SAAS,EAAE,MAAM,KAAK,CAAC,CAAC,KAAK;CAAI,EAAE;CAE7F,EAGF,IAAM,EAAoC,CAAC,EAE3C,IAAK,IAAM,KAAW,EAA+B,uBAAyB,CAAC,EAAG,CAChF,IAAM,EAAK,IAAI,OAAO,CAAO,EACb,EAA8B,KAAM,GAAM,EAAG,KAAK,EAAE,IAAI,CAC7D,GAAG,EAAwB,KAAK,EAAG,SAAS,CAAC,CAC1D,CAEA,GAAI,EAAwB,OAAS,EACnC,MAAO,CACL,aAAcA,EAAAA,aAAa,gCAC3B,iBAAkB;;;EAGtB,EAAwB,IAAK,GAAM,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK;CAAI,EAAE;CAE1D,CAIJ"}
1
+ {"version":3,"file":"judgeByStaticAnalysis.cjs","names":["fs","path","findLanguageDefinitionByPath","removeCommentsInSourceCode","DecisionCode","normalizeCodeRule"],"sources":["../../src/helpers/judgeByStaticAnalysis.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { ProblemMarkdownFrontMatter } from '../types/problem.js';\nimport { normalizeCodeRule } from '../types/problem.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nimport { findLanguageDefinitionByPath } from './findLanguageDefinitionByPath.js';\nimport { removeCommentsInSourceCode } from './removeCommentsInSourceCode.js';\n\ninterface ForbiddenRuleViolation {\n /** Learner-facing description of the rule; the raw pattern when the problem declares no message. */\n label: string;\n matches: { path: string; match: string }[];\n}\n\nexport async function judgeByStaticAnalysis(\n cwd: string,\n problemMarkdownFrontMatterLike: Pick<\n ProblemMarkdownFrontMatter,\n 'forbiddenRegExpsInCode' | 'forbiddenTextsInCode' | 'requiredRegExpsInCode' | 'requiredSubmissionFilePaths'\n >\n): Promise<Pick<TestCaseResult, 'decisionCode' | 'feedbackMarkdown'> | undefined> {\n const filePathSet = new Set<string>();\n const sourceCodeWithoutCommentFiles: { path: string; data: string }[] = [];\n\n for (const dirent of await fs.promises.readdir(cwd, { withFileTypes: true, recursive: true })) {\n if (!dirent.isFile()) continue;\n\n const relativePath = path.relative(cwd, path.join(dirent.parentPath, dirent.name));\n filePathSet.add(relativePath);\n\n const text = await fs.promises.readFile(path.join(dirent.parentPath, dirent.name), 'utf8');\n const isBinary = text.includes('\\uFFFD');\n if (isBinary) continue;\n\n const languageDefinition = findLanguageDefinitionByPath(dirent.name);\n if (!languageDefinition) continue;\n\n sourceCodeWithoutCommentFiles.push({\n path: relativePath,\n data: languageDefinition.grammer ? removeCommentsInSourceCode(languageDefinition.grammer, text) : text,\n });\n }\n\n if (problemMarkdownFrontMatterLike.requiredSubmissionFilePaths) {\n const missingFilePaths = problemMarkdownFrontMatterLike.requiredSubmissionFilePaths\n .filter((p) => !filePathSet.has(p))\n .toSorted();\n\n if (missingFilePaths.length > 0) {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n feedbackMarkdown: `ファイルが不足しています。\n次のファイルを追加してから再度提出してください。\n\n${missingFilePaths.map((p) => `- \\`${p}\\``).join('\\n')}\n`,\n };\n }\n }\n\n const violations: ForbiddenRuleViolation[] = [];\n\n for (const rule of problemMarkdownFrontMatterLike.forbiddenRegExpsInCode ?? []) {\n const { pattern, message } = normalizeCodeRule(rule);\n const re = new RegExp(pattern, 'g');\n const matches = sourceCodeWithoutCommentFiles.flatMap((file) =>\n [...file.data.matchAll(re)].map((match) => ({ path: file.path, match: match[0] }))\n );\n if (matches.length > 0) violations.push({ label: message ?? `禁止パターン \\`${re.toString()}\\``, matches });\n }\n for (const rule of problemMarkdownFrontMatterLike.forbiddenTextsInCode ?? []) {\n const { pattern, message } = normalizeCodeRule(rule);\n const matches = sourceCodeWithoutCommentFiles\n .filter((file) => file.data.includes(pattern))\n .map((file) => ({ path: file.path, match: pattern }));\n if (matches.length > 0) violations.push({ label: message ?? `禁止文字列 \\`${pattern}\\``, matches });\n }\n\n if (violations.length > 0) {\n return {\n decisionCode: DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,\n feedbackMarkdown: `ソースコード中に禁止された文字列が含まれています。\nソースコードを修正してから再度提出してください。\n\n${violations.map(formatForbiddenRuleViolation).join('\\n')}\n`,\n };\n }\n\n const missingRequiredLabels: string[] = [];\n\n for (const rule of problemMarkdownFrontMatterLike.requiredRegExpsInCode ?? []) {\n const { pattern, message } = normalizeCodeRule(rule);\n const re = new RegExp(pattern);\n const isFound = sourceCodeWithoutCommentFiles.some((f) => re.test(f.data));\n if (!isFound) missingRequiredLabels.push(message ?? `\\`${re.toString()}\\``);\n }\n\n if (missingRequiredLabels.length > 0) {\n return {\n decisionCode: DecisionCode.REQUIRED_PATTERNS_IN_CODE_ERROR,\n feedbackMarkdown: `ソースコード中に必要な文字列が含まれていません。\nソースコードを修正してから再度提出してください。\n\n${missingRequiredLabels.map((label) => `- ${label}`).join('\\n')}\n`,\n };\n }\n\n return;\n}\n\nfunction formatForbiddenRuleViolation(violation: ForbiddenRuleViolation): string {\n const locations = [...new Set(violation.matches.map(({ path, match }) => ` - \\`${path}\\`: \\`${match}\\``))];\n return `- ${violation.label}\\n${locations.join('\\n')}`;\n}\n"],"mappings":"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"}