@exercode/problem-utils 4.3.2 → 4.3.4

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 (49) 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/cli/exercodeProblem.cjs +1 -1
  6. package/dist/cli/exercodeProblem.cjs.map +1 -1
  7. package/dist/cli/exercodeProblem.js +1 -1
  8. package/dist/cli/exercodeProblem.js.map +1 -1
  9. package/dist/helpers/guiRecording.cjs +1 -1
  10. package/dist/helpers/guiRecording.cjs.map +1 -1
  11. package/dist/helpers/guiRecording.js +1 -1
  12. package/dist/helpers/guiRecording.js.map +1 -1
  13. package/dist/helpers/javaJudge.cjs +1 -1
  14. package/dist/helpers/javaJudge.cjs.map +1 -1
  15. package/dist/helpers/javaJudge.js +1 -1
  16. package/dist/helpers/javaJudge.js.map +1 -1
  17. package/dist/helpers/parseCsv.cjs +2 -2
  18. package/dist/helpers/parseCsv.cjs.map +1 -1
  19. package/dist/helpers/parseCsv.js +2 -2
  20. package/dist/helpers/parseCsv.js.map +1 -1
  21. package/dist/helpers/removeCommentsInSourceCode.cjs +4 -4
  22. package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
  23. package/dist/helpers/removeCommentsInSourceCode.js +4 -4
  24. package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
  25. package/dist/helpers/runHarnessProcess.cjs +1 -1
  26. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  27. package/dist/helpers/runHarnessProcess.js +1 -1
  28. package/dist/helpers/runHarnessProcess.js.map +1 -1
  29. package/dist/helpers/spawnWithLimits.cjs +1 -1
  30. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  31. package/dist/helpers/spawnWithLimits.js +1 -1
  32. package/dist/helpers/spawnWithLimits.js.map +1 -1
  33. package/dist/helpers/startHttpServer.cjs +1 -1
  34. package/dist/helpers/startHttpServer.cjs.map +1 -1
  35. package/dist/helpers/startHttpServer.js +1 -1
  36. package/dist/helpers/startHttpServer.js.map +1 -1
  37. package/dist/learningMaterial/validateCourse.cjs +1 -1
  38. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  39. package/dist/learningMaterial/validateCourse.js +1 -1
  40. package/dist/learningMaterial/validateCourse.js.map +1 -1
  41. package/dist/learningMaterial/validateProblem.cjs +1 -1
  42. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  43. package/dist/learningMaterial/validateProblem.js +1 -1
  44. package/dist/learningMaterial/validateProblem.js.map +1 -1
  45. package/dist/presets/guiCommand.cjs +1 -1
  46. package/dist/presets/guiCommand.cjs.map +1 -1
  47. package/dist/presets/guiCommand.js +1 -1
  48. package/dist/presets/guiCommand.js.map +1 -1
  49. package/package.json +7 -8
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../helpers/defaultStdioHarness.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../helpers/temporaryProblemDirCopy.cjs"),a=require("../helpers/readProblemMarkdownFrontMatter.cjs"),o=require("../types/decisionCode.cjs"),s=require("../helpers/runHarnessProcess.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("./runSingleHarness.cjs");let u=require("node:path");u=e.__toESM(u,1);let d=require("node:fs/promises");d=e.__toESM(d,1);let f=require("@willbooster/shared-lib");const p=1e3,m=new Map(Object.entries(o.DecisionCode).map(([e,t])=>[t,e]));async function h(e){let t=v(e),i=u.default.resolve(t.rootDir),o=e=>(u.default.relative(i,e)||`.`).replaceAll(u.default.sep,`/`),s=await y(i),d=s.filter(e=>{let n=o(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(d.length===0)return console.error(s.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${s.length} problem directories under ${i} were excluded by --only/--skip.`),1;let p=[],m=[];for(let e of d){let t=await n.findDefaultStdioHarnessFiles(e);if(t.length>0){p.push(`${o(e)}: ${l.formatDefaultHarnessError(t)}`);continue}let i=await c.findModelAnswerDirs(e);if(i.length===0){p.push(`${o(e)}: no model answers found under model_answers/`);continue}if(!await x(u.default.join(e,`judge.ts`))&&(await r.readTestCases(u.default.join(e,`test_cases`))).length===0){let t;try{t=await a.readProblemMarkdownFrontMatter(e)}catch(t){p.push(`${o(e)}: failed to read the problem markdown front matter: ${(0,f.getErrorMessage)(t)}`);continue}if(!a.judgesWithoutTestCases(t)){p.push(`${o(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let s=await c.findFailingModelAnswerDirs(e);m.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...s.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of p)console.error(`✗ ${e}`);let h=u.default.resolve(process.argv[1]??``),_=0;return await(0,f.forEachConcurrently)(m,t.concurrency,async e=>{let t=`${o(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await g(e,h);n===void 0?(_++,console.info(`✓ ${t}`)):(p.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${_} passed, ${p.length} failed (${m.length} runs, ${d.length} problems)`),p.length===0?0:1}async function g(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return S(`failed to copy the problem directory to a temporary location: ${(0,f.getErrorMessage)(e)}`)}let a,o;try{a=_(e,await s.runHarnessProcess([`run`,t,`judge`,u.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){a=S(`harness execution failed: ${(0,f.getErrorMessage)(e)}`)}finally{o=await i.forciblyRemoveDirectory(n)}if(o)return a;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?c:S(`${a}; ${c}`)}function _(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return S(n.failureReason);if(n.exitCode!==0)return S(`harness exited with ${n.exitCode??`a signal`}${i.trim()?`: ${i.trim()}`:``}`);let a=r.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of a){let n;try{n=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)))}catch{n=void 0}if(!n?.success)return S(`malformed test case result line: ${e}`);s.push(n.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==o.DecisionCode.ACCEPTED);return e?S(`${m.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===o.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function v(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function y(e){let t=[];return await b(e,t),t.toSorted()}async function b(e,t){let n=await d.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await b(u.default.join(e,r.name),t)}async function x(e){try{return await d.default.stat(e),!0}catch{return!1}}function S(e){return e.length<=p?e:`${e.slice(0,p)}...`}exports.checkAllProblems=h;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/testCaseResult.cjs"),n=require("../helpers/defaultStdioHarness.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../helpers/temporaryProblemDirCopy.cjs"),a=require("../helpers/readProblemMarkdownFrontMatter.cjs"),o=require("../types/decisionCode.cjs"),s=require("../helpers/runHarnessProcess.cjs"),c=require("../helpers/findModelAnswerDirs.cjs"),l=require("./runSingleHarness.cjs");let u=require("node:path");u=e.__toESM(u,1);let d=require("node:fs/promises");d=e.__toESM(d,1);let f=require("@willbooster/shared-lib");const p=1e3,m=new Map(Object.entries(o.DecisionCode).map(([e,t])=>[t,e]));async function h(e){let t=v(e),i=u.default.resolve(t.rootDir),o=e=>(u.default.relative(i,e)||`.`).replaceAll(u.default.sep,`/`),s=await y(i),d=s.filter(e=>{let n=o(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(d.length===0)return console.error(s.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${s.length} problem directories under ${i} were excluded by --only/--skip.`),1;let p=[],m=[];for(let e of d){let t=await n.findDefaultStdioHarnessFiles(e);if(t.length>0){p.push(`${o(e)}: ${l.formatDefaultHarnessError(t)}`);continue}let i=await c.findModelAnswerDirs(e);if(i.length===0){p.push(`${o(e)}: no model answers found under model_answers/`);continue}if(!await x(u.default.join(e,`judge.ts`))&&(await r.readTestCases(u.default.join(e,`test_cases`))).length===0){let t;try{t=await a.readProblemMarkdownFrontMatter(e)}catch(t){p.push(`${o(e)}: failed to read the problem markdown front matter: ${(0,f.getErrorMessage)(t)}`);continue}if(!a.judgesWithoutTestCases(t)){p.push(`${o(e)}: ${l.MISSING_TEST_CASES_ERROR}`);continue}}let s=await c.findFailingModelAnswerDirs(e);m.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...s.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of p)console.error(`✗ ${e}`);let h=u.default.resolve(process.argv[1]??``),_=0;return await(0,f.forEachConcurrently)(m,t.concurrency,async e=>{let t=`${o(e.problemDir)} ${u.default.relative(e.problemDir,e.answerDir).replaceAll(u.default.sep,`/`)}`,n=await g(e,h);n===void 0?(_++,console.info(`✓ ${t}`)):(p.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${_} passed, ${p.length} failed (${m.length} runs, ${d.length} problems)`),p.length===0?0:1}async function g(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i.copyProblemDirToTemporaryRoot(e.problemDir))}catch(e){return S(`failed to copy the problem directory to a temporary location: ${(0,f.getErrorMessage)(e)}`)}let a,o;try{a=_(e,await s.runHarnessProcess([`run`,t,`judge`,u.default.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){a=S(`harness execution failed: ${(0,f.getErrorMessage)(e)}`)}finally{o=await i.forciblyRemoveDirectory(n)}if(o)return a;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return a===void 0?c:S(`${a}; ${c}`)}function _(e,n){let{stdout:r,stderr:i}=n;if(n.failureReason!==void 0)return S(n.failureReason);if(n.exitCode!==0)return S(`harness exited with ${n.exitCode??`a signal`}${i.trim()?`: ${i.trim()}`:``}`);let a=r.split(/\r?\n/).filter(e=>e.startsWith(t.TEST_CASE_RESULT_PREFIX)),s=[];for(let e of a){let n;try{n=t.testCaseResultSchema.safeParse(JSON.parse(e.slice(t.TEST_CASE_RESULT_PREFIX.length)))}catch{n=void 0}if(!n?.success)return S(`malformed test case result line: ${e}`);s.push(n.data)}if(s.length===0)return`no test case results were printed`;if(e.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==o.DecisionCode.ACCEPTED);return e?S(`${m.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===o.DecisionCode.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function v(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function y(e){let t=[];return await b(e,t),t.toSorted()}async function b(e,t){let n=await d.default.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`))))t.push(e);else for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await b(u.default.join(e,r.name),t)}async function x(e){try{return await d.default.stat(e),!0}catch{return!1}}function S(e){return e.length<=p?e:`${e.slice(0,p)}...`}exports.checkAllProblems=h;
2
2
  //# sourceMappingURL=checkAllProblems.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","forEachConcurrently","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","fs","require_runtime","require_testCaseResult","require_defaultStdioHarness","require_readTestCases","require_temporaryProblemDirCopy","require_readProblemMarkdownFrontMatter","require_decisionCode","require_runHarnessProcess","require_findModelAnswerDirs","require_runSingleHarness","node_path","node_fs_promises","_willbooster_shared_lib","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","defaultHarnessFileNames","modelAnswerDirs","fileExists","frontMatter","error","failingModelAnswerDirs","failure","cliEntryPath","passedCount","label","run","failureDetail","executeCheckRun","tempRoot","copiedProblemDir","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","removalFailureDetail","result","stderr","resultLines","stdout","line","testCaseResults","parsedResult","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\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: ${getErrorMessage(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"checkAllProblems.cjs","names":["DecisionCode","path","findDefaultStdioHarnessFiles","formatDefaultHarnessError","findModelAnswerDirs","readTestCases","readProblemMarkdownFrontMatter","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","findFailingModelAnswerDirs","forEachConcurrently","copyProblemDirToTemporaryRoot","runHarnessProcess","forciblyRemoveDirectory","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","fs","require_runtime","require_testCaseResult","require_defaultStdioHarness","require_readTestCases","require_temporaryProblemDirCopy","require_readProblemMarkdownFrontMatter","require_decisionCode","require_runHarnessProcess","require_findModelAnswerDirs","require_runSingleHarness","node_path","node_fs_promises","_willbooster_shared_lib","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","defaultHarnessFileNames","modelAnswerDirs","fileExists","frontMatter","error","failingModelAnswerDirs","failure","cliEntryPath","passedCount","label","run","failureDetail","executeCheckRun","tempRoot","copiedProblemDir","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","removalFailureDetail","result","stderr","resultLines","stdout","line","testCaseResults","parsedResult","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\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: ${getErrorMessage(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{findDefaultStdioHarnessFiles as n}from"../helpers/defaultStdioHarness.js";import{readTestCases as r}from"../helpers/readTestCases.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"../helpers/temporaryProblemDirCopy.js";import{judgesWithoutTestCases as o,readProblemMarkdownFrontMatter as s}from"../helpers/readProblemMarkdownFrontMatter.js";import{DecisionCode as c}from"../types/decisionCode.js";import{runHarnessProcess as l}from"../helpers/runHarnessProcess.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:path";import h from"node:fs/promises";import{forEachConcurrently as g,getErrorMessage as _}from"@willbooster/shared-lib";const v=1e3,y=new Map(Object.entries(c).map(([e,t])=>[t,e]));async function b(e){let t=C(e),i=m.resolve(t.rootDir),a=e=>(m.relative(i,e)||`.`).replaceAll(m.sep,`/`),c=await w(i),l=c.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(l.length===0)return console.error(c.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${c.length} problem directories under ${i} were excluded by --only/--skip.`),1;let h=[],v=[];for(let e of l){let t=await n(e);if(t.length>0){h.push(`${a(e)}: ${p(t)}`);continue}let i=await d(e);if(i.length===0){h.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await E(m.join(e,`judge.ts`))&&(await r(m.join(e,`test_cases`))).length===0){let t;try{t=await s(e)}catch(t){h.push(`${a(e)}: failed to read the problem markdown front matter: ${_(t)}`);continue}if(!o(t)){h.push(`${a(e)}: ${f}`);continue}}let c=await u(e);v.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...c.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of h)console.error(`✗ ${e}`);let y=m.resolve(process.argv[1]??``),b=0;return await g(v,t.concurrency,async e=>{let t=`${a(e.problemDir)} ${m.relative(e.problemDir,e.answerDir).replaceAll(m.sep,`/`)}`,n=await x(e,y);n===void 0?(b++,console.info(`✓ ${t}`)):(h.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${b} passed, ${h.length} failed (${v.length} runs, ${l.length} problems)`),h.length===0?0:1}async function x(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return D(`failed to copy the problem directory to a temporary location: ${_(e)}`)}let o,s;try{o=S(e,await l([`run`,t,`judge`,m.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=D(`harness execution failed: ${_(e)}`)}finally{s=await a(n)}if(s)return o;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return o===void 0?c:D(`${o}; ${c}`)}function S(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return D(r.failureReason);if(r.exitCode!==0)return D(`harness exited with ${r.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let o=i.split(/\r?\n/).filter(t=>t.startsWith(e)),s=[];for(let n of o){let r;try{r=t.safeParse(JSON.parse(n.slice(e.length)))}catch{r=void 0}if(!r?.success)return D(`malformed test case result line: ${n}`);s.push(r.data)}if(s.length===0)return`no test case results were printed`;if(n.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==c.ACCEPTED);return e?D(`${y.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===c.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function C(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function w(e){let t=[];return await T(e,t),t.toSorted()}async function T(e,t){let n=await h.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`)))){t.push(e);return}for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await T(m.join(e,r.name),t)}async function E(e){try{return await h.stat(e),!0}catch{return!1}}function D(e){return e.length<=v?e:`${e.slice(0,v)}...`}export{b as checkAllProblems};
1
+ import{TEST_CASE_RESULT_PREFIX as e,testCaseResultSchema as t}from"../types/testCaseResult.js";import{findDefaultStdioHarnessFiles as n}from"../helpers/defaultStdioHarness.js";import{readTestCases as r}from"../helpers/readTestCases.js";import{copyProblemDirToTemporaryRoot as i,forciblyRemoveDirectory as a}from"../helpers/temporaryProblemDirCopy.js";import{judgesWithoutTestCases as o,readProblemMarkdownFrontMatter as s}from"../helpers/readProblemMarkdownFrontMatter.js";import{DecisionCode as c}from"../types/decisionCode.js";import{runHarnessProcess as l}from"../helpers/runHarnessProcess.js";import{findFailingModelAnswerDirs as u,findModelAnswerDirs as d}from"../helpers/findModelAnswerDirs.js";import{MISSING_TEST_CASES_ERROR as f,formatDefaultHarnessError as p}from"./runSingleHarness.js";import m from"node:path";import h from"node:fs/promises";import{forEachConcurrently as g,getErrorMessage as _}from"@willbooster/shared-lib";const v=1e3,y=new Map(Object.entries(c).map(([e,t])=>[t,e]));async function b(e){let t=C(e),i=m.resolve(t.rootDir),a=e=>(m.relative(i,e)||`.`).replaceAll(m.sep,`/`),c=await w(i),l=c.filter(e=>{let n=a(e);return t.only.length>0&&!t.only.some(e=>n.includes(e))?!1:!t.skip.some(e=>n.includes(e))});if(l.length===0)return console.error(c.length===0?`No problem directories (containing problem.md or <id>.problem.md) found under ${i}.`:`All ${c.length} problem directories under ${i} were excluded by --only/--skip.`),1;let h=[],v=[];for(let e of l){let t=await n(e);if(t.length>0){h.push(`${a(e)}: ${p(t)}`);continue}let i=await d(e);if(i.length===0){h.push(`${a(e)}: no model answers found under model_answers/`);continue}if(!await E(m.join(e,`judge.ts`))&&(await r(m.join(e,`test_cases`))).length===0){let t;try{t=await s(e)}catch(t){h.push(`${a(e)}: failed to read the problem markdown front matter: ${_(t)}`);continue}if(!o(t)){h.push(`${a(e)}: ${f}`);continue}}let c=await u(e);v.push(...i.map(t=>({problemDir:e,answerDir:t,expectation:`accepted`})),...c.map(t=>({problemDir:e,answerDir:t,expectation:`rejected`})))}for(let e of h)console.error(`✗ ${e}`);let y=m.resolve(process.argv[1]??``),b=0;return await g(v,t.concurrency,async e=>{let t=`${a(e.problemDir)} ${m.relative(e.problemDir,e.answerDir).replaceAll(m.sep,`/`)}`,n=await x(e,y);n===void 0?(b++,console.info(`✓ ${t}`)):(h.push(`${t}: ${n}`),console.error(`✗ ${t}: ${n}`))}),console.info(`\n${b} passed, ${h.length} failed (${v.length} runs, ${l.length} problems)`),h.length===0?0:1}async function x(e,t){let n,r;try{({tempRoot:n,copiedProblemDir:r}=await i(e.problemDir))}catch(e){return D(`failed to copy the problem directory to a temporary location: ${_(e)}`)}let o,s;try{o=S(e,await l([`run`,t,`judge`,m.relative(e.problemDir,e.answerDir)],{cwd:r,env:process.env,timeoutMs:6e5,maxOutputBytes:67108864,tempRoot:n}))}catch(e){o=D(`harness execution failed: ${_(e)}`)}finally{s=await a(n)}if(s)return o;let c=`failed to remove the temporary copy at ${n} (judged code may have left permission-locked files)`;return o===void 0?c:D(`${o}; ${c}`)}function S(n,r){let{stdout:i,stderr:a}=r;if(r.failureReason!==void 0)return D(r.failureReason);if(r.exitCode!==0)return D(`harness exited with ${r.exitCode??`a signal`}${a.trim()?`: ${a.trim()}`:``}`);let o=i.split(/\r?\n/).filter(t=>t.startsWith(e)),s=[];for(let n of o){let r;try{r=t.safeParse(JSON.parse(n.slice(e.length)))}catch{r=void 0}if(!r?.success)return D(`malformed test case result line: ${n}`);s.push(r.data)}if(s.length===0)return`no test case results were printed`;if(n.expectation===`accepted`){let e=s.find(e=>e.decisionCode!==c.ACCEPTED);return e?D(`${y.get(e.decisionCode)??e.decisionCode} on test case ${e.testCaseId}${e.stderr?.trim()?`: ${e.stderr.trim()}`:``}`):void 0}return s.every(e=>e.decisionCode===c.ACCEPTED)?`expected at least one failing test case, but all test cases were accepted`:void 0}function C(e){let t={rootDir:`.`,concurrency:1,only:[],skip:[]},n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(i===void 0)break;if(i===`--concurrency`||i===`--only`||i===`--skip`){let n=e[++r];if(n===void 0)throw Error(`${i} requires a value`);if(i===`--concurrency`){if(t.concurrency=Number(n),!Number.isInteger(t.concurrency)||t.concurrency<=0)throw Error(`--concurrency requires a positive integer, but got ${n}`)}else i===`--only`?t.only.push(n):t.skip.push(n)}else if(i.startsWith(`--`))throw Error(`Unknown option: ${i}`);else if(n)throw Error(`Only one root directory can be specified, but got both ${t.rootDir} and ${i}`);else t.rootDir=i,n=!0}return t}async function w(e){let t=[];return await T(e,t),t.toSorted()}async function T(e,t){let n=await h.readdir(e,{withFileTypes:!0});if(n.some(e=>e.isFile()&&(e.name===`problem.md`||e.name.endsWith(`.problem.md`))))t.push(e);else for(let r of n)r.isDirectory()&&r.name!==`node_modules`&&!r.name.startsWith(`.`)&&await T(m.join(e,r.name),t)}async function E(e){try{return await h.stat(e),!0}catch{return!1}}function D(e){return e.length<=v?e:`${e.slice(0,v)}...`}export{b as checkAllProblems};
2
2
  //# sourceMappingURL=checkAllProblems.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkAllProblems.js","names":["path","fs","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","DecisionCode","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","formatDefaultHarnessError","modelAnswerDirs","findModelAnswerDirs","fileExists","readTestCases","frontMatter","readProblemMarkdownFrontMatter","error","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","failingModelAnswerDirs","findFailingModelAnswerDirs","failure","cliEntryPath","passedCount","forEachConcurrently","label","run","failureDetail","executeCheckRun","tempRoot","copiedProblemDir","copyProblemDirToTemporaryRoot","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","runHarnessProcess","forciblyRemoveDirectory","removalFailureDetail","result","stderr","resultLines","stdout","line","TEST_CASE_RESULT_PREFIX","testCaseResults","parsedResult","testCaseResultSchema","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\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: ${getErrorMessage(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"checkAllProblems.js","names":["path","fs","MAX_FAILURE_DETAIL_LENGTH","decisionCodeNames","DecisionCode","name","code","checkAllProblems","options","parseCheckArgs","args","rootDir","toRelative","dir","allProblemDirs","findProblemDirs","problemDirs","relativeDir","problemDir","substring","failures","runs","defaultHarnessFileNames","findDefaultStdioHarnessFiles","formatDefaultHarnessError","modelAnswerDirs","findModelAnswerDirs","fileExists","readTestCases","frontMatter","readProblemMarkdownFrontMatter","error","getErrorMessage","judgesWithoutTestCases","MISSING_TEST_CASES_ERROR","failingModelAnswerDirs","findFailingModelAnswerDirs","failure","cliEntryPath","passedCount","forEachConcurrently","label","run","failureDetail","executeCheckRun","tempRoot","copiedProblemDir","copyProblemDirToTemporaryRoot","truncate","harnessFailureDetail","removedTempRoot","summarizeHarnessFailure","runHarnessProcess","forciblyRemoveDirectory","removalFailureDetail","result","stderr","resultLines","stdout","line","TEST_CASE_RESULT_PREFIX","testCaseResults","parsedResult","testCaseResultSchema","rejectedResult","hasRootDir","index","arg","value","visitDirectory","entries","entry","filePath","text"],"sources":["../../src/cli/checkAllProblems.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { forEachConcurrently, getErrorMessage } from '@willbooster/shared-lib';\n\nimport { findDefaultStdioHarnessFiles } from '../helpers/defaultStdioHarness.js';\nimport { findFailingModelAnswerDirs, findModelAnswerDirs } from '../helpers/findModelAnswerDirs.js';\nimport { judgesWithoutTestCases, readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { type HarnessProcessResult, runHarnessProcess } from '../helpers/runHarnessProcess.js';\nimport { copyProblemDirToTemporaryRoot, forciblyRemoveDirectory } from '../helpers/temporaryProblemDirCopy.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\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: ${getErrorMessage(error)}`\n );\n continue;\n }\n if (!judgesWithoutTestCases(frontMatter)) {\n failures.push(`${toRelative(problemDir)}: ${MISSING_TEST_CASES_ERROR}`);\n continue;\n }\n }\n }\n\n const failingModelAnswerDirs = await findFailingModelAnswerDirs(problemDir);\n runs.push(\n ...modelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'accepted' })),\n ...failingModelAnswerDirs.map((answerDir): CheckRun => ({ problemDir, answerDir, expectation: 'rejected' }))\n );\n }\n for (const failure of failures) console.error(`✗ ${failure}`);\n\n const cliEntryPath = path.resolve(process.argv[1] ?? '');\n let passedCount = 0;\n await forEachConcurrently(runs, options.concurrency, async (run) => {\n const label = `${toRelative(run.problemDir)} ${path.relative(run.problemDir, run.answerDir).replaceAll(path.sep, '/')}`;\n const failureDetail = await executeCheckRun(run, cliEntryPath);\n if (failureDetail === undefined) {\n passedCount++;\n console.info(`✓ ${label}`);\n } else {\n failures.push(`${label}: ${failureDetail}`);\n console.error(`✗ ${label}: ${failureDetail}`);\n }\n });\n\n console.info(\n `\\n${passedCount} passed, ${failures.length} failed (${runs.length} runs, ${problemDirs.length} problems)`\n );\n return failures.length === 0 ? 0 : 1;\n}\n\n/**\n * Run one answer directory through the problem's harness and return a failure detail, if any.\n * The problem directory is copied to a temporary location first so that judging (e.g. build\n * artifacts in answer directories) never modifies the checked repository.\n */\nasync function executeCheckRun(run: CheckRun, cliEntryPath: string): Promise<string | undefined> {\n let tempRoot: string;\n let copiedProblemDir: string;\n try {\n ({ tempRoot, copiedProblemDir } = await copyProblemDirToTemporaryRoot(run.problemDir));\n } catch (error) {\n return truncate(`failed to copy the problem directory to a temporary location: ${getErrorMessage(error)}`);\n }\n\n let harnessFailureDetail;\n let removedTempRoot;\n try {\n const result = await runHarnessProcess(\n ['run', cliEntryPath, 'judge', path.relative(run.problemDir, run.answerDir)],\n {\n cwd: copiedProblemDir,\n env: process.env,\n timeoutMs: RUN_TIMEOUT_MS,\n maxOutputBytes: MAX_RUN_OUTPUT_BYTES,\n tempRoot,\n }\n );\n harnessFailureDetail = summarizeHarnessFailure(run, result);\n } catch (error) {\n harnessFailureDetail = truncate(`harness execution failed: ${getErrorMessage(error)}`);\n } finally {\n removedTempRoot = await forciblyRemoveDirectory(tempRoot);\n }\n if (removedTempRoot) return harnessFailureDetail;\n const removalFailureDetail = `failed to remove the temporary copy at ${tempRoot} (judged code may have left permission-locked files)`;\n return harnessFailureDetail === undefined\n ? removalFailureDetail\n : truncate(`${harnessFailureDetail}; ${removalFailureDetail}`);\n}\n\n/** Return why the harness run failed the check, or `undefined` when it passed. */\nfunction summarizeHarnessFailure(run: CheckRun, result: HarnessProcessResult): string | undefined {\n const { stdout, stderr } = result;\n if (result.failureReason !== undefined) return truncate(result.failureReason);\n if (result.exitCode !== 0) {\n return truncate(`harness exited with ${result.exitCode ?? 'a signal'}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);\n }\n\n const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n const testCaseResults = [];\n for (const line of resultLines) {\n let parsedResult;\n try {\n parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n } catch {\n parsedResult = undefined;\n }\n if (!parsedResult?.success) return truncate(`malformed test case result line: ${line}`);\n testCaseResults.push(parsedResult.data);\n }\n if (testCaseResults.length === 0) return 'no test case results were printed';\n\n if (run.expectation === 'accepted') {\n const rejectedResult = testCaseResults.find((result) => result.decisionCode !== DecisionCode.ACCEPTED);\n if (rejectedResult) {\n return truncate(\n `${decisionCodeNames.get(rejectedResult.decisionCode) ?? rejectedResult.decisionCode} on test case ${rejectedResult.testCaseId}${rejectedResult.stderr?.trim() ? `: ${rejectedResult.stderr.trim()}` : ''}`\n );\n }\n return undefined;\n }\n return testCaseResults.every((result) => result.decisionCode === DecisionCode.ACCEPTED)\n ? 'expected at least one failing test case, but all test cases were accepted'\n : undefined;\n}\n\nfunction parseCheckArgs(args: readonly string[]): CheckOptions {\n const options: CheckOptions = {\n rootDir: '.',\n // Serial by default: judging decides TIME_LIMIT_EXCEEDED from wall-clock time, so parallel\n // runs on a small CI runner could fail timing-sensitive problems non-deterministically.\n concurrency: 1,\n only: [],\n skip: [],\n };\n let hasRootDir = false;\n for (let index = 0; index < args.length; index++) {\n const arg = args[index];\n if (arg === undefined) break;\n if (arg === '--concurrency' || arg === '--only' || arg === '--skip') {\n const value = args[++index];\n if (value === undefined) throw new Error(`${arg} requires a value`);\n if (arg === '--concurrency') {\n options.concurrency = Number(value);\n if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {\n throw new Error(`--concurrency requires a positive integer, but got ${value}`);\n }\n } else if (arg === '--only') {\n options.only.push(value);\n } else {\n options.skip.push(value);\n }\n } else if (arg.startsWith('--')) {\n throw new Error(`Unknown option: ${arg}`);\n } else if (hasRootDir) {\n throw new Error(`Only one root directory can be specified, but got both ${options.rootDir} and ${arg}`);\n } else {\n options.rootDir = arg;\n hasRootDir = true;\n }\n }\n return options;\n}\n\n/** Find directories containing `problem.md` or `<id>.problem.md`, without descending into found problems. */\nasync function findProblemDirs(rootDir: string): Promise<string[]> {\n const problemDirs: string[] = [];\n await visitDirectory(rootDir, problemDirs);\n return problemDirs.toSorted();\n}\n\nasync function visitDirectory(dir: string, problemDirs: string[]): Promise<void> {\n // Traversal errors (e.g. an unreadable subtree) must fail the check: skipping them silently\n // could report a green result while covering only part of the repository.\n const entries = await fs.readdir(dir, { withFileTypes: true });\n if (entries.some((entry) => entry.isFile() && (entry.name === 'problem.md' || entry.name.endsWith('.problem.md')))) {\n problemDirs.push(dir);\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) continue;\n await visitDirectory(path.join(dir, entry.name), problemDirs);\n }\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction truncate(text: string): string {\n return text.length <= MAX_FAILURE_DETAIL_LENGTH ? text : `${text.slice(0, MAX_FAILURE_DETAIL_LENGTH)}...`;\n}\n"],"mappings":"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"}
@@ -11,5 +11,5 @@ directory to run one answer directory, e.g.:
11
11
  exercode-problem debug model_answers/python '{ "stdin": "1 2" }'
12
12
  The \`validate-*\` subcommands check problem directories, a course directory
13
13
  (course.yaml and lecture materials), or a contest (*.contest.yaml) file
14
- without running any program.`;i();async function i(){let i=process.argv.slice(2);if(i[0]===`--help`||i[0]===`-h`){console.info(r);return}try{if(i[0]===`judge`||i[0]===`debug`){if(i.length<2){console.error(r),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e.runSingleHarness(i[0])}else a(i[0])?process.exitCode=await n.validateLearningMaterial(i[0],i.slice(1)):process.exitCode=await t.checkAllProblems(i)}catch(e){console.error(e),process.exitCode=1}}function a(e){return n.VALIDATE_SUBCOMMANDS.includes(e??``)}
14
+ without running any program.`;i();async function i(){let i=process.argv.slice(2);if(i[0]===`--help`||i[0]===`-h`)console.info(r);else try{if(i[0]===`judge`||i[0]===`debug`){if(i.length<2){console.error(r),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e.runSingleHarness(i[0])}else a(i[0])?process.exitCode=await n.validateLearningMaterial(i[0],i.slice(1)):process.exitCode=await t.checkAllProblems(i)}catch(e){console.error(e),process.exitCode=1}}function a(e){return n.VALIDATE_SUBCOMMANDS.includes(e??``)}
15
15
  //# sourceMappingURL=exercodeProblem.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"exercodeProblem.cjs","names":["runSingleHarness","validateLearningMaterial","checkAllProblems","VALIDATE_SUBCOMMANDS","require_runSingleHarness","require_checkAllProblems","require_validateLearningMaterial","usage","main","args","isValidateSubcommand","error","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, type ValidateSubcommand, validateLearningMaterial } from './validateLearningMaterial.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n exercode-problem validate-problem <problemDir>...\n exercode-problem validate-course <courseDir> [--problems-dir <dir>]\n exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'\nThe \\`validate-*\\` subcommands check problem directories, a course directory\n(course.yaml and lecture materials), or a contest (*.contest.yaml) file\nwithout running any program.`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else if (isValidateSubcommand(args[0])) {\n process.exitCode = await validateLearningMaterial(args[0], args.slice(1));\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n\nfunction isValidateSubcommand(arg: string | undefined): arg is ValidateSubcommand {\n return (VALIDATE_SUBCOMMANDS as readonly string[]).includes(arg ?? '');\n}\n"],"mappings":"uIAIMO,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAAM,CAE5C,QAAQ,KAAKF,CAAK,EAClB,MACF,CACA,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBS,EAAK,EAAE,CACnD,MAAWC,EAAqBD,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMR,EAAAA,yBAAyBQ,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBO,CAAI,CAElD,OAASE,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASD,EAAqB,EAAoD,CAChF,OAAQP,EAAAA,qBAA2C,SAASS,GAAO,EAAE,CACvE"}
1
+ {"version":3,"file":"exercodeProblem.cjs","names":["runSingleHarness","validateLearningMaterial","checkAllProblems","VALIDATE_SUBCOMMANDS","require_runSingleHarness","require_checkAllProblems","require_validateLearningMaterial","usage","main","args","isValidateSubcommand","error","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, type ValidateSubcommand, validateLearningMaterial } from './validateLearningMaterial.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n exercode-problem validate-problem <problemDir>...\n exercode-problem validate-course <courseDir> [--problems-dir <dir>]\n exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'\nThe \\`validate-*\\` subcommands check problem directories, a course directory\n(course.yaml and lecture materials), or a contest (*.contest.yaml) file\nwithout running any program.`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else if (isValidateSubcommand(args[0])) {\n process.exitCode = await validateLearningMaterial(args[0], args.slice(1));\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n\nfunction isValidateSubcommand(arg: string | undefined): arg is ValidateSubcommand {\n return (VALIDATE_SUBCOMMANDS as readonly string[]).includes(arg ?? '');\n}\n"],"mappings":"uIAIMO,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAEtC,QAAQ,KAAKF,CAAK,OAGpB,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBS,EAAK,EAAE,CACnD,MAAWC,EAAqBD,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMR,EAAAA,yBAAyBQ,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMP,EAAAA,iBAAiBO,CAAI,CAElD,OAASE,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASD,EAAqB,EAAoD,CAChF,OAAQP,EAAAA,qBAA2C,SAASS,GAAO,EAAE,CACvE"}
@@ -11,5 +11,5 @@ directory to run one answer directory, e.g.:
11
11
  exercode-problem debug model_answers/python '{ "stdin": "1 2" }'
12
12
  The \`validate-*\` subcommands check problem directories, a course directory
13
13
  (course.yaml and lecture materials), or a contest (*.contest.yaml) file
14
- without running any program.`;a();async function a(){let n=process.argv.slice(2);if(n[0]===`--help`||n[0]===`-h`){console.info(i);return}try{if(n[0]===`judge`||n[0]===`debug`){if(n.length<2){console.error(i),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e(n[0])}else o(n[0])?process.exitCode=await r(n[0],n.slice(1)):process.exitCode=await t(n)}catch(e){console.error(e),process.exitCode=1}}function o(e){return n.includes(e??``)}
14
+ without running any program.`;a();async function a(){let n=process.argv.slice(2);if(n[0]===`--help`||n[0]===`-h`)console.info(i);else try{if(n[0]===`judge`||n[0]===`debug`){if(n.length<2){console.error(i),process.exitCode=1;return}process.argv.splice(2,1),process.exitCode=await e(n[0])}else o(n[0])?process.exitCode=await r(n[0],n.slice(1)):process.exitCode=await t(n)}catch(e){console.error(e),process.exitCode=1}}function o(e){return n.includes(e??``)}
15
15
  //# sourceMappingURL=exercodeProblem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exercodeProblem.js","names":["usage","main","args","runSingleHarness","isValidateSubcommand","validateLearningMaterial","checkAllProblems","error","VALIDATE_SUBCOMMANDS","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, type ValidateSubcommand, validateLearningMaterial } from './validateLearningMaterial.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n exercode-problem validate-problem <problemDir>...\n exercode-problem validate-course <courseDir> [--problems-dir <dir>]\n exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'\nThe \\`validate-*\\` subcommands check problem directories, a course directory\n(course.yaml and lecture materials), or a contest (*.contest.yaml) file\nwithout running any program.`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else if (isValidateSubcommand(args[0])) {\n process.exitCode = await validateLearningMaterial(args[0], args.slice(1));\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n\nfunction isValidateSubcommand(arg: string | undefined): arg is ValidateSubcommand {\n return (VALIDATE_SUBCOMMANDS as readonly string[]).includes(arg ?? '');\n}\n"],"mappings":"qNAIA,MAAMA,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAAM,CAE5C,QAAQ,KAAKF,CAAK,EAClB,MACF,CACA,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMG,EAAiBD,EAAK,EAAE,CACnD,MAAWE,EAAqBF,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMG,EAAyBH,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMI,EAAiBJ,CAAI,CAElD,OAASK,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASH,EAAqB,EAAoD,CAChF,OAAQI,EAA2C,SAASC,GAAO,EAAE,CACvE"}
1
+ {"version":3,"file":"exercodeProblem.js","names":["usage","main","args","runSingleHarness","isValidateSubcommand","validateLearningMaterial","checkAllProblems","error","VALIDATE_SUBCOMMANDS","arg"],"sources":["../../src/cli/exercodeProblem.ts"],"sourcesContent":["import { checkAllProblems } from './checkAllProblems.js';\nimport { runSingleHarness } from './runSingleHarness.js';\nimport { VALIDATE_SUBCOMMANDS, type ValidateSubcommand, validateLearningMaterial } from './validateLearningMaterial.js';\n\nconst usage = `Usage: exercode-problem [rootDir] [--only <substring>]... [--skip <substring>]... [--concurrency <n>]\n exercode-problem judge <answerDir> [paramsJson]\n exercode-problem debug <answerDir> [paramsJson]\n exercode-problem validate-problem <problemDir>...\n exercode-problem validate-course <courseDir> [--problems-dir <dir>]\n exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]\n\nWithout a subcommand, judge all model answers of all problems under rootDir\n(default: the current directory). Use \\`judge\\` or \\`debug\\` in a problem\ndirectory to run one answer directory, e.g.:\n exercode-problem debug model_answers/python '{ \"stdin\": \"1 2\" }'\nThe \\`validate-*\\` subcommands check problem directories, a course directory\n(course.yaml and lecture materials), or a contest (*.contest.yaml) file\nwithout running any program.`;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- the CJS build output does not support top-level await\nvoid main();\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n if (args[0] === '--help' || args[0] === '-h') {\n // console.log would be stripped from the built CLI, so print the usage with console.info.\n console.info(usage);\n return;\n }\n try {\n if (args[0] === 'judge' || args[0] === 'debug') {\n if (args.length < 2) {\n console.error(usage);\n process.exitCode = 1;\n return;\n }\n // Harness scripts and presets read the answer directory and params from fixed positions in\n // `process.argv`, so drop the subcommand to restore those positions.\n process.argv.splice(2, 1);\n process.exitCode = await runSingleHarness(args[0]);\n } else if (isValidateSubcommand(args[0])) {\n process.exitCode = await validateLearningMaterial(args[0], args.slice(1));\n } else {\n process.exitCode = await checkAllProblems(args);\n }\n } catch (error) {\n // Print the whole error: harness failures need their stack and `cause`, not just the message.\n console.error(error);\n process.exitCode = 1;\n }\n}\n\nfunction isValidateSubcommand(arg: string | undefined): arg is ValidateSubcommand {\n return (VALIDATE_SUBCOMMANDS as readonly string[]).includes(arg ?? '');\n}\n"],"mappings":"qNAIA,MAAMA,EAAQ;;;;;;;;;;;;;8BAgBTC,EAAK,EAEV,eAAeA,GAAsB,CACnC,IAAMC,EAAO,QAAQ,KAAK,MAAM,CAAC,EACjC,GAAIA,EAAK,KAAO,UAAYA,EAAK,KAAO,KAEtC,QAAQ,KAAKF,CAAK,OAGpB,GAAI,CACF,GAAIE,EAAK,KAAO,SAAWA,EAAK,KAAO,QAAS,CAC9C,GAAIA,EAAK,OAAS,EAAG,CACnB,QAAQ,MAAMF,CAAK,EACnB,QAAQ,SAAW,EACnB,MACF,CAGA,QAAQ,KAAK,OAAO,EAAG,CAAC,EACxB,QAAQ,SAAW,MAAMG,EAAiBD,EAAK,EAAE,CACnD,MAAWE,EAAqBF,EAAK,EAAE,EACrC,QAAQ,SAAW,MAAMG,EAAyBH,EAAK,GAAIA,EAAK,MAAM,CAAC,CAAC,EAExE,QAAQ,SAAW,MAAMI,EAAiBJ,CAAI,CAElD,OAASK,EAAO,CAEd,QAAQ,MAAMA,CAAK,EACnB,QAAQ,SAAW,CACrB,CACF,CAEA,SAASH,EAAqB,EAAoD,CAChF,OAAQI,EAA2C,SAASC,GAAO,EAAE,CACvE"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:crypto");t=e.__toESM(t,1);let n=require("node:zlib");n=e.__toESM(n,1);const r=Buffer.from(`89504e470d0a1a0a`,`hex`),i={0:1,2:3,3:1,4:2,6:4},a=65535;var o=class{#e=/* @__PURE__ */ new Map;add(e,n,r){let i=this.#e.get(e)??{path:``,frames:[],lastDigest:``,changeCount:0,keptChangeInterval:1,lastSeenAtMs:0,isTooLarge:!1};this.#e.set(e,i),i.path=n.path.replace(/\.png$/,`_recording.png`),i.lastSeenAtMs=r;let a=t.default.createHash(`sha256`).update(n.data).digest(`base64`);if(i.lastDigest===a)return;i.lastDigest=a;let o=i.changeCount++;i.isTooLarge||o%i.keptChangeInterval!==0||(i.frames.push({png:Buffer.from(n.data,`base64`),capturedAtMs:r}),this.#t(({frames:e})=>e.reduce((e,t)=>e+t.png.length,0)))}build(e){let t=/* @__PURE__ */ new Map,n=({frames:n,lastSeenAtMs:r})=>{let i=t.get(n)??c(n.map((t,i)=>({png:t.png,delayMs:(n[i+1]?.capturedAtMs??r+e)-t.capturedAtMs})));return t.set(n,i),i},r=e=>s(e)&&e.frames.length>1;return this.#t(e=>r(e)?n(e).length:0),[...this.#e.values()].filter(r).map(e=>({path:e.path,data:n(e).toString(`base64`),encoding:`base64`}))}#t(e){let t=[...this.#e.values()];for(;;){let n=new Map(t.map(t=>[t,e(t)]));if([...n.values()].reduce((e,t)=>e+t,0)<=2097152)return;let r=t.filter(e=>e.frames.length>0).toSorted((e,t)=>(n.get(t)??0)-(n.get(e)??0)),[i]=r.filter(e=>!s(e)).toSorted((e,t)=>e.changeCount-t.changeCount),a=r.find(e=>e.frames.length>2);if(i)i.frames=[];else if(a)a.frames=a.frames.filter((e,t)=>t%2==0),a.keptChangeInterval*=2;else{let[e]=r;e.frames=[],e.isTooLarge=!0}}}};function s(e){return e.changeCount>=3}function c(e){let t=e.map(e=>({...l(e.png),delayMs:e.delayMs})),o=t[0]?.header.subarray(8);if(!o)throw Error(`an animated PNG needs at least one frame`);let s=t.filter(e=>e.header.subarray(8).equals(o)).map(e=>({...e,width:e.header.readUInt32BE(0),height:e.header.readUInt32BE(4)})),[c]=s,d=Math.max(...s.map(e=>e.width)),f=Math.max(...s.map(e=>e.height)),p=Buffer.from(c.header);p.writeUInt32BE(d,0),p.writeUInt32BE(f,4);let m=Buffer.alloc(8);m.writeUInt32BE(s.length,0);let h=[r,u(`IHDR`,p),...c.metadataChunks,u(`acTL`,m)],g=c.width===d&&c.height===f;if(!g){let e=o[0]*(i[o[1]]??1),t=Buffer.alloc(f*(1+Math.ceil(d*e/8)));h.push(u(`IDAT`,n.default.deflateSync(t)))}let _=0;for(let[e,t]of s.entries()){let n=Buffer.alloc(26);n.writeUInt32BE(_++,0),n.writeUInt32BE(t.width,4),n.writeUInt32BE(t.height,8);let r=Math.max(1,Math.min(1e3,Math.floor(a*1e3/t.delayMs))),i=Math.round(t.delayMs*r/1e3);if(n.writeUInt16BE(Math.min(Math.max(i,1),a),20),n.writeUInt16BE(r,22),n.writeUInt8(1,24),h.push(u(`fcTL`,n)),e===0&&g)h.push(u(`IDAT`,t.imageData));else{let e=Buffer.alloc(4);e.writeUInt32BE(_++),h.push(u(`fdAT`,Buffer.concat([e,t.imageData])))}}return h.push(u(`IEND`,Buffer.alloc(0))),Buffer.concat(h)}function l(e){if(!e.subarray(0,r.length).equals(r))throw Error(`not a PNG image`);let t,n=[],i=[],a=r.length;for(;a+12<=e.length;){let r=e.readUInt32BE(a),o=e.toString(`latin1`,a+4,a+8),s=e.subarray(a+8,a+8+r),c=a+12+r;o===`IHDR`?t=s:o===`IDAT`?i.push(s):i.length===0&&o!==`IEND`&&n.push(e.subarray(a,c)),a=c}if(t?.length!==13||i.length===0)throw Error(`not a PNG image`);return{header:t,metadataChunks:n,imageData:Buffer.concat(i)}}function u(e,t){let r=Buffer.concat([Buffer.from(e,`latin1`),t]),i=Buffer.alloc(r.length+8);return i.writeUInt32BE(t.length,0),r.copy(i,4),i.writeUInt32BE(n.default.crc32(r),i.length-4),i}exports.GuiRecorder=o,exports.MAX_GUI_RECORDING_BYTES=2097152,exports.encodeAnimatedPng=c;
1
+ "use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:crypto");t=e.__toESM(t,1);let n=require("node:zlib");n=e.__toESM(n,1);const r=Buffer.from(`89504e470d0a1a0a`,`hex`),i={0:1,2:3,3:1,4:2,6:4},a=65535;var o=class{#e=/* @__PURE__ */ new Map;add(e,n,r){let i=this.#e.get(e)??{path:``,frames:[],lastDigest:``,changeCount:0,keptChangeInterval:1,lastSeenAtMs:0,isTooLarge:!1};this.#e.set(e,i),i.path=n.path.replace(/\.png$/,`_recording.png`),i.lastSeenAtMs=r;let a=t.default.createHash(`sha256`).update(n.data).digest(`base64`);if(i.lastDigest===a)return;i.lastDigest=a;let o=i.changeCount++;i.isTooLarge||o%i.keptChangeInterval!==0||(i.frames.push({png:Buffer.from(n.data,`base64`),capturedAtMs:r}),this.#t(({frames:e})=>e.reduce((e,t)=>e+t.png.length,0)))}build(e){let t=/* @__PURE__ */ new Map,n=({frames:n,lastSeenAtMs:r})=>{let i=t.get(n)??l(n.map((t,i)=>({png:t.png,delayMs:(n[i+1]?.capturedAtMs??r+e)-t.capturedAtMs})));return t.set(n,i),i};return this.#t(e=>s(e)?n(e).length:0),[...this.#e.values()].filter(s).map(e=>({path:e.path,data:n(e).toString(`base64`),encoding:`base64`}))}#t(e){let t=[...this.#e.values()];for(;;){let n=new Map(t.map(t=>[t,e(t)]));if([...n.values()].reduce((e,t)=>e+t,0)<=2097152)return;let r=t.filter(e=>e.frames.length>0).toSorted((e,t)=>(n.get(t)??0)-(n.get(e)??0)),[i]=r.filter(e=>!c(e)).toSorted((e,t)=>e.changeCount-t.changeCount),a=r.find(e=>e.frames.length>2);if(i)i.frames=[];else if(a)a.frames=a.frames.filter((e,t)=>t%2==0),a.keptChangeInterval*=2;else{let[e]=r;e.frames=[],e.isTooLarge=!0}}}};function s(e){return c(e)&&e.frames.length>1}function c(e){return e.changeCount>=3}function l(e){let t=e.map(e=>({...u(e.png),delayMs:e.delayMs})),o=t[0]?.header.subarray(8);if(!o)throw Error(`an animated PNG needs at least one frame`);let s=t.filter(e=>e.header.subarray(8).equals(o)).map(e=>({...e,width:e.header.readUInt32BE(0),height:e.header.readUInt32BE(4)})),[c]=s,l=Math.max(...s.map(e=>e.width)),f=Math.max(...s.map(e=>e.height)),p=Buffer.from(c.header);p.writeUInt32BE(l,0),p.writeUInt32BE(f,4);let m=Buffer.alloc(8);m.writeUInt32BE(s.length,0);let h=[r,d(`IHDR`,p),...c.metadataChunks,d(`acTL`,m)],g=c.width===l&&c.height===f;if(!g){let e=o[0]*(i[o[1]]??1),t=Buffer.alloc(f*(1+Math.ceil(l*e/8)));h.push(d(`IDAT`,n.default.deflateSync(t)))}let _=0;for(let[e,t]of s.entries()){let n=Buffer.alloc(26);n.writeUInt32BE(_++,0),n.writeUInt32BE(t.width,4),n.writeUInt32BE(t.height,8);let r=Math.max(1,Math.min(1e3,Math.floor(a*1e3/t.delayMs))),i=Math.round(t.delayMs*r/1e3);if(n.writeUInt16BE(Math.min(Math.max(i,1),a),20),n.writeUInt16BE(r,22),n.writeUInt8(1,24),h.push(d(`fcTL`,n)),e===0&&g)h.push(d(`IDAT`,t.imageData));else{let e=Buffer.alloc(4);e.writeUInt32BE(_++),h.push(d(`fdAT`,Buffer.concat([e,t.imageData])))}}return h.push(d(`IEND`,Buffer.alloc(0))),Buffer.concat(h)}function u(e){if(!e.subarray(0,r.length).equals(r))throw Error(`not a PNG image`);let t,n=[],i=[],a=r.length;for(;a+12<=e.length;){let r=e.readUInt32BE(a),o=e.toString(`latin1`,a+4,a+8),s=e.subarray(a+8,a+8+r),c=a+12+r;o===`IHDR`?t=s:o===`IDAT`?i.push(s):i.length===0&&o!==`IEND`&&n.push(e.subarray(a,c)),a=c}if(t?.length!==13||i.length===0)throw Error(`not a PNG image`);return{header:t,metadataChunks:n,imageData:Buffer.concat(i)}}function d(e,t){let r=Buffer.concat([Buffer.from(e,`latin1`),t]),i=Buffer.alloc(r.length+8);return i.writeUInt32BE(t.length,0),r.copy(i,4),i.writeUInt32BE(n.default.crc32(r),i.length-4),i}exports.GuiRecorder=o,exports.MAX_GUI_RECORDING_BYTES=2097152,exports.encodeAnimatedPng=l;
2
2
  //# sourceMappingURL=guiRecording.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"guiRecording.cjs","names":["crypto","zlib","require_runtime","node_crypto","node_zlib","PNG_SIGNATURE","CHANNELS_BY_COLOR_TYPE","MAX_UINT16","GuiRecorder","captures","#windowIdToCaptures","windowId","screenshot","capturedAtMs","digest","changeIndex","#shrinkToFit","frames","sum","frame","framesToAnimatedPng","encode","animatedPng","encodeAnimatedPng","index","lastSeenAtMs","frameIntervalMs","isRecorded","isAnimated","allCaptures","sizes","measure","size","bySizeDescending","b","a","notAnimated","shrinkable","largest","parsedFrames","parsePng","pixelFormat","animationFrames","firstFrame","width","height","header","animationControl","chunks","createChunk","startsWithFirstFrame","bitsPerPixel","blankScanlines","sequenceNumber","frameControl","delayUnitsPerSecond","delay","sequence","png","metadataChunks","imageDataChunks","offset","dataLength","type","data","nextOffset","typeAndData","chunk"],"sources":["../../src/helpers/guiRecording.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport zlib from 'node:zlib';\n\n/** Bounds the recordings of one GUI run in total, so a judge server can budget for them. */\nexport const MAX_GUI_RECORDING_BYTES = 2 * 1024 * 1024;\n\nexport interface GuiRecordingFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\ninterface CapturedFrame {\n png: Buffer;\n capturedAtMs: number;\n}\n\ninterface WindowCaptures {\n path: string;\n /** The captures kept for the recording; none once the window turned out too large to record. */\n frames: CapturedFrame[];\n /** A digest of the last capture, kept or not, to tell whether the window changed. */\n lastDigest: string;\n changeCount: number;\n /** Every how many changes a capture is kept; doubled whenever half of the kept frames are dropped. */\n keptChangeInterval: number;\n lastSeenAtMs: number;\n isTooLarge: boolean;\n}\n\ninterface AnimationFrame {\n png: Buffer;\n delayMs: number;\n}\n\ninterface ParsedPng {\n /** The IHDR data: width, height, and the 5-byte pixel format. */\n header: Buffer;\n /** The chunks between IHDR and the image data (e.g. a palette), as they are in the file. */\n metadataChunks: Buffer[];\n imageData: Buffer;\n}\n\nconst PNG_SIGNATURE = Buffer.from('89504e470d0a1a0a', 'hex');\nconst CHANNELS_BY_COLOR_TYPE: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };\nconst MAX_UINT16 = 65_535;\n// A window is typically captured blank once before its first paint, which is not an animation.\nconst MIN_RECORDED_CHANGE_COUNT = 3;\n\n/**\n * Collects the screenshots of each window over a run and turns the windows that kept changing into\n * animated PNGs, which browsers play wherever they show a PNG. The captures it keeps are bounded\n * like the recordings, however long the run lasts and whatever the windows show.\n */\nexport class GuiRecorder {\n readonly #windowIdToCaptures = new Map<string, WindowCaptures>();\n\n add(windowId: string, screenshot: { path: string; data: string }, capturedAtMs: number): void {\n const captures = this.#windowIdToCaptures.get(windowId) ?? {\n path: '',\n frames: [],\n lastDigest: '',\n changeCount: 0,\n keptChangeInterval: 1,\n lastSeenAtMs: 0,\n isTooLarge: false,\n };\n this.#windowIdToCaptures.set(windowId, captures);\n captures.path = screenshot.path.replace(/\\.png$/, '_recording.png');\n captures.lastSeenAtMs = capturedAtMs;\n // An unchanged window only extends how long its last frame is shown.\n const digest = crypto.createHash('sha256').update(screenshot.data).digest('base64');\n if (captures.lastDigest === digest) return;\n\n captures.lastDigest = digest;\n const changeIndex = captures.changeCount++;\n if (captures.isTooLarge || changeIndex % captures.keptChangeInterval !== 0) return;\n\n captures.frames.push({ png: Buffer.from(screenshot.data, 'base64'), capturedAtMs });\n this.#shrinkToFit(({ frames }) => frames.reduce((sum, frame) => sum + frame.png.length, 0));\n }\n\n /** @param frameIntervalMs How long the last capture of a window is shown. */\n build(frameIntervalMs: number): GuiRecordingFile[] {\n const framesToAnimatedPng = new Map<CapturedFrame[], Buffer>();\n const encode = ({ frames, lastSeenAtMs }: WindowCaptures): Buffer => {\n const animatedPng =\n framesToAnimatedPng.get(frames) ??\n encodeAnimatedPng(\n frames.map((frame, index) => ({\n png: frame.png,\n delayMs: (frames[index + 1]?.capturedAtMs ?? lastSeenAtMs + frameIntervalMs) - frame.capturedAtMs,\n }))\n );\n framesToAnimatedPng.set(frames, animatedPng);\n return animatedPng;\n };\n const isRecorded = (captures: WindowCaptures): boolean => isAnimated(captures) && captures.frames.length > 1;\n\n // The chunks an animated PNG adds per frame can exceed what the frames alone were bounded to.\n this.#shrinkToFit((captures) => (isRecorded(captures) ? encode(captures).length : 0));\n return [...this.#windowIdToCaptures.values()].filter(isRecorded).map((captures) => ({\n path: captures.path,\n data: encode(captures).toString('base64'),\n encoding: 'base64',\n }));\n }\n\n /**\n * Frees space until all windows together fit `MAX_GUI_RECORDING_BYTES`, giving up what matters\n * least first: the early captures of a window that is not animated (yet), then every other frame\n * of the largest animated window, and only when no window has more than two frames a whole window,\n * which is then not recorded.\n */\n #shrinkToFit(measure: (captures: WindowCaptures) => number): void {\n const allCaptures = [...this.#windowIdToCaptures.values()];\n for (;;) {\n const sizes = new Map(allCaptures.map((captures) => [captures, measure(captures)]));\n if ([...sizes.values()].reduce((sum, size) => sum + size, 0) <= MAX_GUI_RECORDING_BYTES) return;\n\n const bySizeDescending = allCaptures\n .filter((captures) => captures.frames.length > 0)\n .toSorted((a, b) => (sizes.get(b) ?? 0) - (sizes.get(a) ?? 0));\n // The fewer changes a window showed, the less likely it is the start of an animation.\n const [notAnimated] = bySizeDescending\n .filter((captures) => !isAnimated(captures))\n .toSorted((a, b) => a.changeCount - b.changeCount);\n const shrinkable = bySizeDescending.find((captures) => captures.frames.length > 2);\n if (notAnimated) {\n notAnimated.frames = [];\n } else if (shrinkable) {\n shrinkable.frames = shrinkable.frames.filter((_, index) => index % 2 === 0);\n shrinkable.keptChangeInterval *= 2;\n } else {\n const [largest] = bySizeDescending as [WindowCaptures, ...WindowCaptures[]];\n largest.frames = [];\n largest.isTooLarge = true;\n }\n }\n }\n}\n\nfunction isAnimated(captures: WindowCaptures): boolean {\n return captures.changeCount >= MIN_RECORDED_CHANGE_COUNT;\n}\n\n/**\n * Assembles PNG images into an animated PNG (APNG) that loops forever. The compressed image data of\n * each frame is reused as is, so the frames must share the pixel format of the first one; frames of\n * another format are left out. A frame smaller than the largest one is drawn at the top-left corner.\n */\nexport function encodeAnimatedPng(frames: readonly AnimationFrame[]): Buffer {\n const parsedFrames = frames.map((frame) => ({ ...parsePng(frame.png), delayMs: frame.delayMs }));\n const pixelFormat = parsedFrames[0]?.header.subarray(8);\n if (!pixelFormat) throw new Error('an animated PNG needs at least one frame');\n const animationFrames = parsedFrames\n .filter((frame) => frame.header.subarray(8).equals(pixelFormat))\n .map((frame) => ({ ...frame, width: frame.header.readUInt32BE(0), height: frame.header.readUInt32BE(4) }));\n const [firstFrame] = animationFrames as [(typeof animationFrames)[number], ...typeof animationFrames];\n const width = Math.max(...animationFrames.map((frame) => frame.width));\n const height = Math.max(...animationFrames.map((frame) => frame.height));\n\n const header = Buffer.from(firstFrame.header);\n header.writeUInt32BE(width, 0);\n header.writeUInt32BE(height, 4);\n const animationControl = Buffer.alloc(8);\n animationControl.writeUInt32BE(animationFrames.length, 0);\n const chunks = [\n PNG_SIGNATURE,\n createChunk('IHDR', header),\n ...firstFrame.metadataChunks,\n createChunk('acTL', animationControl),\n ];\n\n // The image shown by viewers without animation support must fill the canvas. Only a first frame\n // of that size can serve as it; otherwise it is a blank image that is not part of the animation.\n const startsWithFirstFrame = firstFrame.width === width && firstFrame.height === height;\n if (!startsWithFirstFrame) {\n const bitsPerPixel = (pixelFormat[0] as number) * (CHANNELS_BY_COLOR_TYPE[pixelFormat[1] as number] ?? 1);\n const blankScanlines = Buffer.alloc(height * (1 + Math.ceil((width * bitsPerPixel) / 8)));\n chunks.push(createChunk('IDAT', zlib.deflateSync(blankScanlines)));\n }\n\n let sequenceNumber = 0;\n for (const [index, frame] of animationFrames.entries()) {\n const frameControl = Buffer.alloc(26);\n frameControl.writeUInt32BE(sequenceNumber++, 0);\n frameControl.writeUInt32BE(frame.width, 4);\n frameControl.writeUInt32BE(frame.height, 8);\n // The x and y offsets at 12 and 16 stay 0.\n // The delay is a fraction of two 16-bit integers: milliseconds, or the finest unit a longer delay fits in.\n const delayUnitsPerSecond = Math.max(1, Math.min(1000, Math.floor((MAX_UINT16 * 1000) / frame.delayMs)));\n const delay = Math.round((frame.delayMs * delayUnitsPerSecond) / 1000);\n frameControl.writeUInt16BE(Math.min(Math.max(delay, 1), MAX_UINT16), 20);\n frameControl.writeUInt16BE(delayUnitsPerSecond, 22);\n // Every frame is cleared before the next one, so a larger frame does not stay visible around a smaller one.\n frameControl.writeUInt8(1, 24);\n chunks.push(createChunk('fcTL', frameControl));\n\n if (index === 0 && startsWithFirstFrame) {\n chunks.push(createChunk('IDAT', frame.imageData));\n } else {\n const sequence = Buffer.alloc(4);\n sequence.writeUInt32BE(sequenceNumber++);\n chunks.push(createChunk('fdAT', Buffer.concat([sequence, frame.imageData])));\n }\n }\n chunks.push(createChunk('IEND', Buffer.alloc(0)));\n return Buffer.concat(chunks);\n}\n\nfunction parsePng(png: Buffer): ParsedPng {\n if (!png.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error('not a PNG image');\n\n let header: Buffer | undefined;\n const metadataChunks: Buffer[] = [];\n const imageDataChunks: Buffer[] = [];\n let offset = PNG_SIGNATURE.length;\n while (offset + 12 <= png.length) {\n const dataLength = png.readUInt32BE(offset);\n const type = png.toString('latin1', offset + 4, offset + 8);\n const data = png.subarray(offset + 8, offset + 8 + dataLength);\n const nextOffset = offset + 12 + dataLength;\n if (type === 'IHDR') {\n header = data;\n } else if (type === 'IDAT') {\n imageDataChunks.push(data);\n } else if (imageDataChunks.length === 0 && type !== 'IEND') {\n metadataChunks.push(png.subarray(offset, nextOffset));\n }\n offset = nextOffset;\n }\n if (header?.length !== 13 || imageDataChunks.length === 0) throw new Error('not a PNG image');\n return { header, metadataChunks, imageData: Buffer.concat(imageDataChunks) };\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n const typeAndData = Buffer.concat([Buffer.from(type, 'latin1'), data]);\n const chunk = Buffer.alloc(typeAndData.length + 8);\n chunk.writeUInt32BE(data.length, 0);\n typeAndData.copy(chunk, 4);\n chunk.writeUInt32BE(zlib.crc32(typeAndData), chunk.length - 4);\n return chunk;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"guiRecording.cjs","names":["crypto","zlib","require_runtime","node_crypto","node_zlib","PNG_SIGNATURE","CHANNELS_BY_COLOR_TYPE","MAX_UINT16","GuiRecorder","captures","#windowIdToCaptures","windowId","screenshot","capturedAtMs","digest","changeIndex","#shrinkToFit","frames","sum","frame","framesToAnimatedPng","encode","animatedPng","encodeAnimatedPng","index","lastSeenAtMs","frameIntervalMs","isRecorded","allCaptures","sizes","measure","size","bySizeDescending","b","a","notAnimated","isAnimated","shrinkable","largest","parsedFrames","parsePng","pixelFormat","animationFrames","firstFrame","width","height","header","animationControl","chunks","createChunk","startsWithFirstFrame","bitsPerPixel","blankScanlines","sequenceNumber","frameControl","delayUnitsPerSecond","delay","sequence","png","metadataChunks","imageDataChunks","offset","dataLength","type","data","nextOffset","typeAndData","chunk"],"sources":["../../src/helpers/guiRecording.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport zlib from 'node:zlib';\n\n/** Bounds the recordings of one GUI run in total, so a judge server can budget for them. */\nexport const MAX_GUI_RECORDING_BYTES = 2 * 1024 * 1024;\n\nexport interface GuiRecordingFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\ninterface CapturedFrame {\n png: Buffer;\n capturedAtMs: number;\n}\n\ninterface WindowCaptures {\n path: string;\n /** The captures kept for the recording; none once the window turned out too large to record. */\n frames: CapturedFrame[];\n /** A digest of the last capture, kept or not, to tell whether the window changed. */\n lastDigest: string;\n changeCount: number;\n /** Every how many changes a capture is kept; doubled whenever half of the kept frames are dropped. */\n keptChangeInterval: number;\n lastSeenAtMs: number;\n isTooLarge: boolean;\n}\n\ninterface AnimationFrame {\n png: Buffer;\n delayMs: number;\n}\n\ninterface ParsedPng {\n /** The IHDR data: width, height, and the 5-byte pixel format. */\n header: Buffer;\n /** The chunks between IHDR and the image data (e.g. a palette), as they are in the file. */\n metadataChunks: Buffer[];\n imageData: Buffer;\n}\n\nconst PNG_SIGNATURE = Buffer.from('89504e470d0a1a0a', 'hex');\nconst CHANNELS_BY_COLOR_TYPE: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };\nconst MAX_UINT16 = 65_535;\n// A window is typically captured blank once before its first paint, which is not an animation.\nconst MIN_RECORDED_CHANGE_COUNT = 3;\n\n/**\n * Collects the screenshots of each window over a run and turns the windows that kept changing into\n * animated PNGs, which browsers play wherever they show a PNG. The captures it keeps are bounded\n * like the recordings, however long the run lasts and whatever the windows show.\n */\nexport class GuiRecorder {\n readonly #windowIdToCaptures = new Map<string, WindowCaptures>();\n\n add(windowId: string, screenshot: { path: string; data: string }, capturedAtMs: number): void {\n const captures = this.#windowIdToCaptures.get(windowId) ?? {\n path: '',\n frames: [],\n lastDigest: '',\n changeCount: 0,\n keptChangeInterval: 1,\n lastSeenAtMs: 0,\n isTooLarge: false,\n };\n this.#windowIdToCaptures.set(windowId, captures);\n captures.path = screenshot.path.replace(/\\.png$/, '_recording.png');\n captures.lastSeenAtMs = capturedAtMs;\n // An unchanged window only extends how long its last frame is shown.\n const digest = crypto.createHash('sha256').update(screenshot.data).digest('base64');\n if (captures.lastDigest === digest) return;\n\n captures.lastDigest = digest;\n const changeIndex = captures.changeCount++;\n if (captures.isTooLarge || changeIndex % captures.keptChangeInterval !== 0) return;\n\n captures.frames.push({ png: Buffer.from(screenshot.data, 'base64'), capturedAtMs });\n this.#shrinkToFit(({ frames }) => frames.reduce((sum, frame) => sum + frame.png.length, 0));\n }\n\n /** @param frameIntervalMs How long the last capture of a window is shown. */\n build(frameIntervalMs: number): GuiRecordingFile[] {\n const framesToAnimatedPng = new Map<CapturedFrame[], Buffer>();\n const encode = ({ frames, lastSeenAtMs }: WindowCaptures): Buffer => {\n const animatedPng =\n framesToAnimatedPng.get(frames) ??\n encodeAnimatedPng(\n frames.map((frame, index) => ({\n png: frame.png,\n delayMs: (frames[index + 1]?.capturedAtMs ?? lastSeenAtMs + frameIntervalMs) - frame.capturedAtMs,\n }))\n );\n framesToAnimatedPng.set(frames, animatedPng);\n return animatedPng;\n };\n // The chunks an animated PNG adds per frame can exceed what the frames alone were bounded to.\n this.#shrinkToFit((captures) => (isRecorded(captures) ? encode(captures).length : 0));\n return [...this.#windowIdToCaptures.values()].filter(isRecorded).map((captures) => ({\n path: captures.path,\n data: encode(captures).toString('base64'),\n encoding: 'base64',\n }));\n }\n\n /**\n * Frees space until all windows together fit `MAX_GUI_RECORDING_BYTES`, giving up what matters\n * least first: the early captures of a window that is not animated (yet), then every other frame\n * of the largest animated window, and only when no window has more than two frames a whole window,\n * which is then not recorded.\n */\n #shrinkToFit(measure: (captures: WindowCaptures) => number): void {\n const allCaptures = [...this.#windowIdToCaptures.values()];\n for (;;) {\n const sizes = new Map(allCaptures.map((captures) => [captures, measure(captures)]));\n if ([...sizes.values()].reduce((sum, size) => sum + size, 0) <= MAX_GUI_RECORDING_BYTES) return;\n\n const bySizeDescending = allCaptures\n .filter((captures) => captures.frames.length > 0)\n .toSorted((a, b) => (sizes.get(b) ?? 0) - (sizes.get(a) ?? 0));\n // The fewer changes a window showed, the less likely it is the start of an animation.\n const [notAnimated] = bySizeDescending\n .filter((captures) => !isAnimated(captures))\n .toSorted((a, b) => a.changeCount - b.changeCount);\n const shrinkable = bySizeDescending.find((captures) => captures.frames.length > 2);\n if (notAnimated) {\n notAnimated.frames = [];\n } else if (shrinkable) {\n shrinkable.frames = shrinkable.frames.filter((_, index) => index % 2 === 0);\n shrinkable.keptChangeInterval *= 2;\n } else {\n const [largest] = bySizeDescending as [WindowCaptures, ...WindowCaptures[]];\n largest.frames = [];\n largest.isTooLarge = true;\n }\n }\n }\n}\n\nfunction isRecorded(captures: WindowCaptures): boolean {\n return isAnimated(captures) && captures.frames.length > 1;\n}\n\nfunction isAnimated(captures: WindowCaptures): boolean {\n return captures.changeCount >= MIN_RECORDED_CHANGE_COUNT;\n}\n\n/**\n * Assembles PNG images into an animated PNG (APNG) that loops forever. The compressed image data of\n * each frame is reused as is, so the frames must share the pixel format of the first one; frames of\n * another format are left out. A frame smaller than the largest one is drawn at the top-left corner.\n */\nexport function encodeAnimatedPng(frames: readonly AnimationFrame[]): Buffer {\n const parsedFrames = frames.map((frame) => ({ ...parsePng(frame.png), delayMs: frame.delayMs }));\n const pixelFormat = parsedFrames[0]?.header.subarray(8);\n if (!pixelFormat) throw new Error('an animated PNG needs at least one frame');\n const animationFrames = parsedFrames\n .filter((frame) => frame.header.subarray(8).equals(pixelFormat))\n .map((frame) => ({ ...frame, width: frame.header.readUInt32BE(0), height: frame.header.readUInt32BE(4) }));\n const [firstFrame] = animationFrames as [(typeof animationFrames)[number], ...typeof animationFrames];\n const width = Math.max(...animationFrames.map((frame) => frame.width));\n const height = Math.max(...animationFrames.map((frame) => frame.height));\n\n const header = Buffer.from(firstFrame.header);\n header.writeUInt32BE(width, 0);\n header.writeUInt32BE(height, 4);\n const animationControl = Buffer.alloc(8);\n animationControl.writeUInt32BE(animationFrames.length, 0);\n const chunks = [\n PNG_SIGNATURE,\n createChunk('IHDR', header),\n ...firstFrame.metadataChunks,\n createChunk('acTL', animationControl),\n ];\n\n // The image shown by viewers without animation support must fill the canvas. Only a first frame\n // of that size can serve as it; otherwise it is a blank image that is not part of the animation.\n const startsWithFirstFrame = firstFrame.width === width && firstFrame.height === height;\n if (!startsWithFirstFrame) {\n const bitsPerPixel = (pixelFormat[0] as number) * (CHANNELS_BY_COLOR_TYPE[pixelFormat[1] as number] ?? 1);\n const blankScanlines = Buffer.alloc(height * (1 + Math.ceil((width * bitsPerPixel) / 8)));\n chunks.push(createChunk('IDAT', zlib.deflateSync(blankScanlines)));\n }\n\n let sequenceNumber = 0;\n for (const [index, frame] of animationFrames.entries()) {\n const frameControl = Buffer.alloc(26);\n frameControl.writeUInt32BE(sequenceNumber++, 0);\n frameControl.writeUInt32BE(frame.width, 4);\n frameControl.writeUInt32BE(frame.height, 8);\n // The x and y offsets at 12 and 16 stay 0.\n // The delay is a fraction of two 16-bit integers: milliseconds, or the finest unit a longer delay fits in.\n const delayUnitsPerSecond = Math.max(1, Math.min(1000, Math.floor((MAX_UINT16 * 1000) / frame.delayMs)));\n const delay = Math.round((frame.delayMs * delayUnitsPerSecond) / 1000);\n frameControl.writeUInt16BE(Math.min(Math.max(delay, 1), MAX_UINT16), 20);\n frameControl.writeUInt16BE(delayUnitsPerSecond, 22);\n // Every frame is cleared before the next one, so a larger frame does not stay visible around a smaller one.\n frameControl.writeUInt8(1, 24);\n chunks.push(createChunk('fcTL', frameControl));\n\n if (index === 0 && startsWithFirstFrame) {\n chunks.push(createChunk('IDAT', frame.imageData));\n } else {\n const sequence = Buffer.alloc(4);\n sequence.writeUInt32BE(sequenceNumber++);\n chunks.push(createChunk('fdAT', Buffer.concat([sequence, frame.imageData])));\n }\n }\n chunks.push(createChunk('IEND', Buffer.alloc(0)));\n return Buffer.concat(chunks);\n}\n\nfunction parsePng(png: Buffer): ParsedPng {\n if (!png.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error('not a PNG image');\n\n let header: Buffer | undefined;\n const metadataChunks: Buffer[] = [];\n const imageDataChunks: Buffer[] = [];\n let offset = PNG_SIGNATURE.length;\n while (offset + 12 <= png.length) {\n const dataLength = png.readUInt32BE(offset);\n const type = png.toString('latin1', offset + 4, offset + 8);\n const data = png.subarray(offset + 8, offset + 8 + dataLength);\n const nextOffset = offset + 12 + dataLength;\n if (type === 'IHDR') {\n header = data;\n } else if (type === 'IDAT') {\n imageDataChunks.push(data);\n } else if (imageDataChunks.length === 0 && type !== 'IEND') {\n metadataChunks.push(png.subarray(offset, nextOffset));\n }\n offset = nextOffset;\n }\n if (header?.length !== 13 || imageDataChunks.length === 0) throw new Error('not a PNG image');\n return { header, metadataChunks, imageData: Buffer.concat(imageDataChunks) };\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n const typeAndData = Buffer.concat([Buffer.from(type, 'latin1'), data]);\n const chunk = Buffer.alloc(typeAndData.length + 8);\n chunk.writeUInt32BE(data.length, 0);\n typeAndData.copy(chunk, 4);\n chunk.writeUInt32BE(zlib.crc32(typeAndData), chunk.length - 4);\n return chunk;\n}\n"],"mappings":"4JAIA,MAuCMK,EAAgB,OAAO,KAAK,mBAAoB,KAAK,EACrDC,EAAiD,CAAE,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,CAAE,EAChFC,EAAa,MASnB,IAAaC,EAAb,KAAyB,CACvB,mBAA+B,IAAI,IAEnC,IAAI,EAAkB,EAA4C,EAA4B,CAC5F,IAAMC,EAAW,KAAKC,GAAoB,IAAIC,CAAQ,GAAK,CACzD,KAAM,GACN,OAAQ,CAAC,EACT,WAAY,GACZ,YAAa,EACb,mBAAoB,EACpB,aAAc,EACd,WAAY,EACd,EACA,KAAKD,GAAoB,IAAIC,EAAUF,CAAQ,EAC/C,EAAS,KAAOG,EAAW,KAAK,QAAQ,SAAU,gBAAgB,EAClE,EAAS,aAAeC,EAExB,IAAMC,EAASd,EAAAA,QAAO,WAAW,QAAQ,CAAC,CAAC,OAAOY,EAAW,IAAI,CAAC,CAAC,OAAO,QAAQ,EAClF,GAAIH,EAAS,aAAeK,EAAQ,OAEpC,EAAS,WAAaA,EACtB,IAAMC,EAAc,EAAS,cACzBN,EAAS,YAAcM,EAAcN,EAAS,qBAAuB,IAEzEA,EAAS,OAAO,KAAK,CAAE,IAAK,OAAO,KAAKG,EAAW,KAAM,QAAQ,EAAG,cAAa,CAAC,EAClF,KAAKI,IAAc,CAAE,YAAaC,EAAO,QAAQ,EAAK,IAAUC,EAAMC,EAAM,IAAI,OAAQ,CAAC,CAAC,EAC5F,CAGA,MAAM,EAA6C,CACjD,IAAMC,kBAAsB,IAAI,IAC1BC,GAAU,CAAE,SAAQ,kBAA2C,CACnE,IAAMC,EACJF,EAAoB,IAAIH,CAAM,GAC9BM,EACEN,EAAO,KAAK,EAAO,KAAW,CAC5B,IAAKE,EAAM,IACX,SAAUF,EAAOO,EAAQ,EAAE,EAAE,cAAgBC,EAAeC,GAAmBP,EAAM,YACvF,EAAE,CACJ,EAEF,OADAC,EAAoB,IAAIH,EAAQK,CAAW,EACpCA,CACT,EAGA,OADA,KAAKN,GAAc,GAAcW,EAAWlB,CAAQ,EAAIY,EAAOZ,CAAQ,CAAC,CAAC,OAAS,CAAE,EAC7E,CAAC,GAAG,KAAKC,GAAoB,OAAO,CAAC,CAAC,CAAC,OAAOiB,CAAU,CAAC,CAAC,IAAK,IAAc,CAClF,KAAMlB,EAAS,KACf,KAAMY,EAAOZ,CAAQ,CAAC,CAAC,SAAS,QAAQ,EACxC,SAAU,QACZ,EAAE,CACJ,CAQA,GAAa,EAAqD,CAChE,IAAMmB,EAAc,CAAC,GAAG,KAAKlB,GAAoB,OAAO,CAAC,EACzD,OAAS,CACP,IAAMmB,EAAQ,IAAI,IAAID,EAAY,IAAK,GAAa,CAACnB,EAAUqB,EAAQrB,CAAQ,CAAC,CAAC,CAAC,EAClF,GAAI,CAAC,GAAGoB,EAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAK,IAASX,EAAMa,EAAM,CAAC,GAAA,QAA8B,OAEzF,IAAMC,EAAmBJ,EACtB,OAAQ,GAAanB,EAAS,OAAO,OAAS,CAAC,CAAC,CAChD,UAAU,EAAG,KAAOoB,EAAM,IAAII,CAAC,GAAK,IAAMJ,EAAM,IAAIK,CAAC,GAAK,EAAE,EAEzD,CAACC,GAAeH,EACnB,OAAQ,GAAa,CAACI,EAAW3B,CAAQ,CAAC,CAAC,CAC3C,UAAU,EAAG,IAAMyB,EAAE,YAAcD,EAAE,WAAW,EAC7CI,EAAaL,EAAiB,KAAM,GAAavB,EAAS,OAAO,OAAS,CAAC,EACjF,GAAI0B,EACF,EAAY,OAAS,CAAC,OACjB,GAAIE,EACT,EAAW,OAASA,EAAW,OAAO,QAAQ,EAAG,IAAUb,EAAQ,GAAM,CAAC,EAC1E,EAAW,oBAAsB,MAC5B,CACL,GAAM,CAACc,GAAWN,EAClB,EAAQ,OAAS,CAAC,EAClB,EAAQ,WAAa,EACvB,CACF,CACF,CACF,EAEA,SAASL,EAAW,EAAmC,CACrD,OAAOS,EAAW3B,CAAQ,GAAKA,EAAS,OAAO,OAAS,CAC1D,CAEA,SAAS2B,EAAW,EAAmC,CACrD,OAAO3B,EAAS,aAAe,CACjC,CAOA,SAAgBc,EAAkB,EAA2C,CAC3E,IAAMgB,EAAetB,EAAO,IAAK,IAAW,CAAE,GAAGuB,EAASrB,EAAM,GAAG,EAAG,QAASA,EAAM,OAAQ,EAAE,EACzFsB,EAAcF,EAAa,EAAE,EAAE,OAAO,SAAS,CAAC,EACtD,GAAI,CAACE,EAAa,MAAU,MAAM,0CAA0C,EAC5E,IAAMC,EAAkBH,EACrB,OAAQ,GAAUpB,EAAM,OAAO,SAAS,CAAC,CAAC,CAAC,OAAOsB,CAAW,CAAC,CAAC,CAC/D,IAAK,IAAW,CAAE,GAAGtB,EAAO,MAAOA,EAAM,OAAO,aAAa,CAAC,EAAG,OAAQA,EAAM,OAAO,aAAa,CAAC,CAAE,EAAE,EACrG,CAACwB,GAAcD,EACfE,EAAQ,KAAK,IAAI,GAAGF,EAAgB,IAAK,GAAUvB,EAAM,KAAK,CAAC,EAC/D0B,EAAS,KAAK,IAAI,GAAGH,EAAgB,IAAK,GAAUvB,EAAM,MAAM,CAAC,EAEjE2B,EAAS,OAAO,KAAKH,EAAW,MAAM,EAC5CG,EAAO,cAAcF,EAAO,CAAC,EAC7BE,EAAO,cAAcD,EAAQ,CAAC,EAC9B,IAAME,EAAmB,OAAO,MAAM,CAAC,EACvCA,EAAiB,cAAcL,EAAgB,OAAQ,CAAC,EACxD,IAAMM,EAAS,CACb3C,EACA4C,EAAY,OAAQH,CAAM,EAC1B,GAAGH,EAAW,eACdM,EAAY,OAAQF,CAAgB,CACtC,EAIMG,EAAuBP,EAAW,QAAUC,GAASD,EAAW,SAAWE,EACjF,GAAI,CAACK,EAAsB,CACzB,IAAMC,EAAgBV,EAAY,IAAiBnC,EAAuBmC,EAAY,KAAiB,GACjGW,EAAiB,OAAO,MAAMP,GAAU,EAAI,KAAK,KAAMD,EAAQO,EAAgB,CAAC,EAAE,EACxFH,EAAO,KAAKC,EAAY,OAAQhD,EAAAA,QAAK,YAAYmD,CAAc,CAAC,CAAC,CACnE,CAEA,IAAIC,EAAiB,EACrB,IAAK,GAAM,CAAC7B,EAAOL,KAAUuB,EAAgB,QAAQ,EAAG,CACtD,IAAMY,EAAe,OAAO,MAAM,EAAE,EACpCA,EAAa,cAAc,IAAkB,CAAC,EAC9CA,EAAa,cAAcnC,EAAM,MAAO,CAAC,EACzCmC,EAAa,cAAcnC,EAAM,OAAQ,CAAC,EAG1C,IAAMoC,EAAsB,KAAK,IAAI,EAAG,KAAK,IAAI,IAAM,KAAK,MAAOhD,EAAa,IAAQY,EAAM,OAAO,CAAC,CAAC,EACjGqC,EAAQ,KAAK,MAAOrC,EAAM,QAAUoC,EAAuB,GAAI,EAOrE,GANAD,EAAa,cAAc,KAAK,IAAI,KAAK,IAAIE,EAAO,CAAC,EAAGjD,CAAU,EAAG,EAAE,EACvE+C,EAAa,cAAcC,EAAqB,EAAE,EAElDD,EAAa,WAAW,EAAG,EAAE,EAC7BN,EAAO,KAAKC,EAAY,OAAQK,CAAY,CAAC,EAEzC9B,IAAU,GAAK0B,EACjBF,EAAO,KAAKC,EAAY,OAAQ9B,EAAM,SAAS,CAAC,MAC3C,CACL,IAAMsC,EAAW,OAAO,MAAM,CAAC,EAC/BA,EAAS,cAAc,GAAgB,EACvCT,EAAO,KAAKC,EAAY,OAAQ,OAAO,OAAO,CAACQ,EAAUtC,EAAM,SAAS,CAAC,CAAC,CAAC,CAC7E,CACF,CAEA,OADA6B,EAAO,KAAKC,EAAY,OAAQ,OAAO,MAAM,CAAC,CAAC,CAAC,EACzC,OAAO,OAAOD,CAAM,CAC7B,CAEA,SAASR,EAAS,EAAwB,CACxC,GAAI,CAACkB,EAAI,SAAS,EAAGrD,EAAc,MAAM,CAAC,CAAC,OAAOA,CAAa,EAAG,MAAU,MAAM,iBAAiB,EAEnG,IAAIyC,EACEa,EAA2B,CAAC,EAC5BC,EAA4B,CAAC,EAC/BC,EAASxD,EAAc,OAC3B,KAAOwD,EAAS,IAAMH,EAAI,QAAQ,CAChC,IAAMI,EAAaJ,EAAI,aAAaG,CAAM,EACpCE,EAAOL,EAAI,SAAS,SAAUG,EAAS,EAAGA,EAAS,CAAC,EACpDG,EAAON,EAAI,SAASG,EAAS,EAAGA,EAAS,EAAIC,CAAU,EACvDG,EAAaJ,EAAS,GAAKC,EAC7BC,IAAS,OACX,EAASC,EACAD,IAAS,OAClBH,EAAgB,KAAKI,CAAI,EAChBJ,EAAgB,SAAW,GAAKG,IAAS,QAClDJ,EAAe,KAAKD,EAAI,SAASG,EAAQI,CAAU,CAAC,EAEtD,EAASA,CACX,CACA,GAAInB,GAAQ,SAAW,IAAMc,EAAgB,SAAW,EAAG,MAAU,MAAM,iBAAiB,EAC5F,MAAO,CAAE,SAAQ,iBAAgB,UAAW,OAAO,OAAOA,CAAe,CAAE,CAC7E,CAEA,SAASX,EAAY,EAAc,EAAsB,CACvD,IAAMiB,EAAc,OAAO,OAAO,CAAC,OAAO,KAAKH,EAAM,QAAQ,EAAGC,CAAI,CAAC,EAC/DG,EAAQ,OAAO,MAAMD,EAAY,OAAS,CAAC,EAIjD,OAHAC,EAAM,cAAcH,EAAK,OAAQ,CAAC,EAClCE,EAAY,KAAKC,EAAO,CAAC,EACzBA,EAAM,cAAclE,EAAAA,QAAK,MAAMiE,CAAW,EAAGC,EAAM,OAAS,CAAC,EACtDA,CACT"}
@@ -1,2 +1,2 @@
1
- import e from"node:crypto";import t from"node:zlib";const n=2097152,r=Buffer.from(`89504e470d0a1a0a`,`hex`),i={0:1,2:3,3:1,4:2,6:4},a=65535;var o=class{#e=/* @__PURE__ */ new Map;add(t,n,r){let i=this.#e.get(t)??{path:``,frames:[],lastDigest:``,changeCount:0,keptChangeInterval:1,lastSeenAtMs:0,isTooLarge:!1};this.#e.set(t,i),i.path=n.path.replace(/\.png$/,`_recording.png`),i.lastSeenAtMs=r;let a=e.createHash(`sha256`).update(n.data).digest(`base64`);if(i.lastDigest===a)return;i.lastDigest=a;let o=i.changeCount++;i.isTooLarge||o%i.keptChangeInterval!==0||(i.frames.push({png:Buffer.from(n.data,`base64`),capturedAtMs:r}),this.#t(({frames:e})=>e.reduce((e,t)=>e+t.png.length,0)))}build(e){let t=/* @__PURE__ */ new Map,n=({frames:n,lastSeenAtMs:r})=>{let i=t.get(n)??c(n.map((t,i)=>({png:t.png,delayMs:(n[i+1]?.capturedAtMs??r+e)-t.capturedAtMs})));return t.set(n,i),i},r=e=>s(e)&&e.frames.length>1;return this.#t(e=>r(e)?n(e).length:0),[...this.#e.values()].filter(r).map(e=>({path:e.path,data:n(e).toString(`base64`),encoding:`base64`}))}#t(e){let t=[...this.#e.values()];for(;;){let n=new Map(t.map(t=>[t,e(t)]));if([...n.values()].reduce((e,t)=>e+t,0)<=2097152)return;let r=t.filter(e=>e.frames.length>0).toSorted((e,t)=>(n.get(t)??0)-(n.get(e)??0)),[i]=r.filter(e=>!s(e)).toSorted((e,t)=>e.changeCount-t.changeCount),a=r.find(e=>e.frames.length>2);if(i)i.frames=[];else if(a)a.frames=a.frames.filter((e,t)=>t%2==0),a.keptChangeInterval*=2;else{let[e]=r;e.frames=[],e.isTooLarge=!0}}}};function s(e){return e.changeCount>=3}function c(e){let n=e.map(e=>({...l(e.png),delayMs:e.delayMs})),o=n[0]?.header.subarray(8);if(!o)throw Error(`an animated PNG needs at least one frame`);let s=n.filter(e=>e.header.subarray(8).equals(o)).map(e=>({...e,width:e.header.readUInt32BE(0),height:e.header.readUInt32BE(4)})),[c]=s,d=Math.max(...s.map(e=>e.width)),f=Math.max(...s.map(e=>e.height)),p=Buffer.from(c.header);p.writeUInt32BE(d,0),p.writeUInt32BE(f,4);let m=Buffer.alloc(8);m.writeUInt32BE(s.length,0);let h=[r,u(`IHDR`,p),...c.metadataChunks,u(`acTL`,m)],g=c.width===d&&c.height===f;if(!g){let e=o[0]*(i[o[1]]??1),n=Buffer.alloc(f*(1+Math.ceil(d*e/8)));h.push(u(`IDAT`,t.deflateSync(n)))}let _=0;for(let[e,t]of s.entries()){let n=Buffer.alloc(26);n.writeUInt32BE(_++,0),n.writeUInt32BE(t.width,4),n.writeUInt32BE(t.height,8);let r=Math.max(1,Math.min(1e3,Math.floor(a*1e3/t.delayMs))),i=Math.round(t.delayMs*r/1e3);if(n.writeUInt16BE(Math.min(Math.max(i,1),a),20),n.writeUInt16BE(r,22),n.writeUInt8(1,24),h.push(u(`fcTL`,n)),e===0&&g)h.push(u(`IDAT`,t.imageData));else{let e=Buffer.alloc(4);e.writeUInt32BE(_++),h.push(u(`fdAT`,Buffer.concat([e,t.imageData])))}}return h.push(u(`IEND`,Buffer.alloc(0))),Buffer.concat(h)}function l(e){if(!e.subarray(0,r.length).equals(r))throw Error(`not a PNG image`);let t,n=[],i=[],a=r.length;for(;a+12<=e.length;){let r=e.readUInt32BE(a),o=e.toString(`latin1`,a+4,a+8),s=e.subarray(a+8,a+8+r),c=a+12+r;o===`IHDR`?t=s:o===`IDAT`?i.push(s):i.length===0&&o!==`IEND`&&n.push(e.subarray(a,c)),a=c}if(t?.length!==13||i.length===0)throw Error(`not a PNG image`);return{header:t,metadataChunks:n,imageData:Buffer.concat(i)}}function u(e,n){let r=Buffer.concat([Buffer.from(e,`latin1`),n]),i=Buffer.alloc(r.length+8);return i.writeUInt32BE(n.length,0),r.copy(i,4),i.writeUInt32BE(t.crc32(r),i.length-4),i}export{o as GuiRecorder,n as MAX_GUI_RECORDING_BYTES,c as encodeAnimatedPng};
1
+ import e from"node:crypto";import t from"node:zlib";const n=2097152,r=Buffer.from(`89504e470d0a1a0a`,`hex`),i={0:1,2:3,3:1,4:2,6:4},a=65535;var o=class{#e=/* @__PURE__ */ new Map;add(t,n,r){let i=this.#e.get(t)??{path:``,frames:[],lastDigest:``,changeCount:0,keptChangeInterval:1,lastSeenAtMs:0,isTooLarge:!1};this.#e.set(t,i),i.path=n.path.replace(/\.png$/,`_recording.png`),i.lastSeenAtMs=r;let a=e.createHash(`sha256`).update(n.data).digest(`base64`);if(i.lastDigest===a)return;i.lastDigest=a;let o=i.changeCount++;i.isTooLarge||o%i.keptChangeInterval!==0||(i.frames.push({png:Buffer.from(n.data,`base64`),capturedAtMs:r}),this.#t(({frames:e})=>e.reduce((e,t)=>e+t.png.length,0)))}build(e){let t=/* @__PURE__ */ new Map,n=({frames:n,lastSeenAtMs:r})=>{let i=t.get(n)??l(n.map((t,i)=>({png:t.png,delayMs:(n[i+1]?.capturedAtMs??r+e)-t.capturedAtMs})));return t.set(n,i),i};return this.#t(e=>s(e)?n(e).length:0),[...this.#e.values()].filter(s).map(e=>({path:e.path,data:n(e).toString(`base64`),encoding:`base64`}))}#t(e){let t=[...this.#e.values()];for(;;){let n=new Map(t.map(t=>[t,e(t)]));if([...n.values()].reduce((e,t)=>e+t,0)<=2097152)return;let r=t.filter(e=>e.frames.length>0).toSorted((e,t)=>(n.get(t)??0)-(n.get(e)??0)),[i]=r.filter(e=>!c(e)).toSorted((e,t)=>e.changeCount-t.changeCount),a=r.find(e=>e.frames.length>2);if(i)i.frames=[];else if(a)a.frames=a.frames.filter((e,t)=>t%2==0),a.keptChangeInterval*=2;else{let[e]=r;e.frames=[],e.isTooLarge=!0}}}};function s(e){return c(e)&&e.frames.length>1}function c(e){return e.changeCount>=3}function l(e){let n=e.map(e=>({...u(e.png),delayMs:e.delayMs})),o=n[0]?.header.subarray(8);if(!o)throw Error(`an animated PNG needs at least one frame`);let s=n.filter(e=>e.header.subarray(8).equals(o)).map(e=>({...e,width:e.header.readUInt32BE(0),height:e.header.readUInt32BE(4)})),[c]=s,l=Math.max(...s.map(e=>e.width)),f=Math.max(...s.map(e=>e.height)),p=Buffer.from(c.header);p.writeUInt32BE(l,0),p.writeUInt32BE(f,4);let m=Buffer.alloc(8);m.writeUInt32BE(s.length,0);let h=[r,d(`IHDR`,p),...c.metadataChunks,d(`acTL`,m)],g=c.width===l&&c.height===f;if(!g){let e=o[0]*(i[o[1]]??1),n=Buffer.alloc(f*(1+Math.ceil(l*e/8)));h.push(d(`IDAT`,t.deflateSync(n)))}let _=0;for(let[e,t]of s.entries()){let n=Buffer.alloc(26);n.writeUInt32BE(_++,0),n.writeUInt32BE(t.width,4),n.writeUInt32BE(t.height,8);let r=Math.max(1,Math.min(1e3,Math.floor(a*1e3/t.delayMs))),i=Math.round(t.delayMs*r/1e3);if(n.writeUInt16BE(Math.min(Math.max(i,1),a),20),n.writeUInt16BE(r,22),n.writeUInt8(1,24),h.push(d(`fcTL`,n)),e===0&&g)h.push(d(`IDAT`,t.imageData));else{let e=Buffer.alloc(4);e.writeUInt32BE(_++),h.push(d(`fdAT`,Buffer.concat([e,t.imageData])))}}return h.push(d(`IEND`,Buffer.alloc(0))),Buffer.concat(h)}function u(e){if(!e.subarray(0,r.length).equals(r))throw Error(`not a PNG image`);let t,n=[],i=[],a=r.length;for(;a+12<=e.length;){let r=e.readUInt32BE(a),o=e.toString(`latin1`,a+4,a+8),s=e.subarray(a+8,a+8+r),c=a+12+r;o===`IHDR`?t=s:o===`IDAT`?i.push(s):i.length===0&&o!==`IEND`&&n.push(e.subarray(a,c)),a=c}if(t?.length!==13||i.length===0)throw Error(`not a PNG image`);return{header:t,metadataChunks:n,imageData:Buffer.concat(i)}}function d(e,n){let r=Buffer.concat([Buffer.from(e,`latin1`),n]),i=Buffer.alloc(r.length+8);return i.writeUInt32BE(n.length,0),r.copy(i,4),i.writeUInt32BE(t.crc32(r),i.length-4),i}export{o as GuiRecorder,n as MAX_GUI_RECORDING_BYTES,l as encodeAnimatedPng};
2
2
  //# sourceMappingURL=guiRecording.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"guiRecording.js","names":["crypto","zlib","MAX_GUI_RECORDING_BYTES","PNG_SIGNATURE","CHANNELS_BY_COLOR_TYPE","MAX_UINT16","GuiRecorder","captures","#windowIdToCaptures","windowId","screenshot","capturedAtMs","digest","changeIndex","#shrinkToFit","frames","sum","frame","framesToAnimatedPng","encode","animatedPng","encodeAnimatedPng","index","lastSeenAtMs","frameIntervalMs","isRecorded","isAnimated","allCaptures","sizes","measure","size","bySizeDescending","b","a","notAnimated","shrinkable","largest","parsedFrames","parsePng","pixelFormat","animationFrames","firstFrame","width","height","header","animationControl","chunks","createChunk","startsWithFirstFrame","bitsPerPixel","blankScanlines","sequenceNumber","frameControl","delayUnitsPerSecond","delay","sequence","png","metadataChunks","imageDataChunks","offset","dataLength","type","data","nextOffset","typeAndData","chunk"],"sources":["../../src/helpers/guiRecording.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport zlib from 'node:zlib';\n\n/** Bounds the recordings of one GUI run in total, so a judge server can budget for them. */\nexport const MAX_GUI_RECORDING_BYTES = 2 * 1024 * 1024;\n\nexport interface GuiRecordingFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\ninterface CapturedFrame {\n png: Buffer;\n capturedAtMs: number;\n}\n\ninterface WindowCaptures {\n path: string;\n /** The captures kept for the recording; none once the window turned out too large to record. */\n frames: CapturedFrame[];\n /** A digest of the last capture, kept or not, to tell whether the window changed. */\n lastDigest: string;\n changeCount: number;\n /** Every how many changes a capture is kept; doubled whenever half of the kept frames are dropped. */\n keptChangeInterval: number;\n lastSeenAtMs: number;\n isTooLarge: boolean;\n}\n\ninterface AnimationFrame {\n png: Buffer;\n delayMs: number;\n}\n\ninterface ParsedPng {\n /** The IHDR data: width, height, and the 5-byte pixel format. */\n header: Buffer;\n /** The chunks between IHDR and the image data (e.g. a palette), as they are in the file. */\n metadataChunks: Buffer[];\n imageData: Buffer;\n}\n\nconst PNG_SIGNATURE = Buffer.from('89504e470d0a1a0a', 'hex');\nconst CHANNELS_BY_COLOR_TYPE: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };\nconst MAX_UINT16 = 65_535;\n// A window is typically captured blank once before its first paint, which is not an animation.\nconst MIN_RECORDED_CHANGE_COUNT = 3;\n\n/**\n * Collects the screenshots of each window over a run and turns the windows that kept changing into\n * animated PNGs, which browsers play wherever they show a PNG. The captures it keeps are bounded\n * like the recordings, however long the run lasts and whatever the windows show.\n */\nexport class GuiRecorder {\n readonly #windowIdToCaptures = new Map<string, WindowCaptures>();\n\n add(windowId: string, screenshot: { path: string; data: string }, capturedAtMs: number): void {\n const captures = this.#windowIdToCaptures.get(windowId) ?? {\n path: '',\n frames: [],\n lastDigest: '',\n changeCount: 0,\n keptChangeInterval: 1,\n lastSeenAtMs: 0,\n isTooLarge: false,\n };\n this.#windowIdToCaptures.set(windowId, captures);\n captures.path = screenshot.path.replace(/\\.png$/, '_recording.png');\n captures.lastSeenAtMs = capturedAtMs;\n // An unchanged window only extends how long its last frame is shown.\n const digest = crypto.createHash('sha256').update(screenshot.data).digest('base64');\n if (captures.lastDigest === digest) return;\n\n captures.lastDigest = digest;\n const changeIndex = captures.changeCount++;\n if (captures.isTooLarge || changeIndex % captures.keptChangeInterval !== 0) return;\n\n captures.frames.push({ png: Buffer.from(screenshot.data, 'base64'), capturedAtMs });\n this.#shrinkToFit(({ frames }) => frames.reduce((sum, frame) => sum + frame.png.length, 0));\n }\n\n /** @param frameIntervalMs How long the last capture of a window is shown. */\n build(frameIntervalMs: number): GuiRecordingFile[] {\n const framesToAnimatedPng = new Map<CapturedFrame[], Buffer>();\n const encode = ({ frames, lastSeenAtMs }: WindowCaptures): Buffer => {\n const animatedPng =\n framesToAnimatedPng.get(frames) ??\n encodeAnimatedPng(\n frames.map((frame, index) => ({\n png: frame.png,\n delayMs: (frames[index + 1]?.capturedAtMs ?? lastSeenAtMs + frameIntervalMs) - frame.capturedAtMs,\n }))\n );\n framesToAnimatedPng.set(frames, animatedPng);\n return animatedPng;\n };\n const isRecorded = (captures: WindowCaptures): boolean => isAnimated(captures) && captures.frames.length > 1;\n\n // The chunks an animated PNG adds per frame can exceed what the frames alone were bounded to.\n this.#shrinkToFit((captures) => (isRecorded(captures) ? encode(captures).length : 0));\n return [...this.#windowIdToCaptures.values()].filter(isRecorded).map((captures) => ({\n path: captures.path,\n data: encode(captures).toString('base64'),\n encoding: 'base64',\n }));\n }\n\n /**\n * Frees space until all windows together fit `MAX_GUI_RECORDING_BYTES`, giving up what matters\n * least first: the early captures of a window that is not animated (yet), then every other frame\n * of the largest animated window, and only when no window has more than two frames a whole window,\n * which is then not recorded.\n */\n #shrinkToFit(measure: (captures: WindowCaptures) => number): void {\n const allCaptures = [...this.#windowIdToCaptures.values()];\n for (;;) {\n const sizes = new Map(allCaptures.map((captures) => [captures, measure(captures)]));\n if ([...sizes.values()].reduce((sum, size) => sum + size, 0) <= MAX_GUI_RECORDING_BYTES) return;\n\n const bySizeDescending = allCaptures\n .filter((captures) => captures.frames.length > 0)\n .toSorted((a, b) => (sizes.get(b) ?? 0) - (sizes.get(a) ?? 0));\n // The fewer changes a window showed, the less likely it is the start of an animation.\n const [notAnimated] = bySizeDescending\n .filter((captures) => !isAnimated(captures))\n .toSorted((a, b) => a.changeCount - b.changeCount);\n const shrinkable = bySizeDescending.find((captures) => captures.frames.length > 2);\n if (notAnimated) {\n notAnimated.frames = [];\n } else if (shrinkable) {\n shrinkable.frames = shrinkable.frames.filter((_, index) => index % 2 === 0);\n shrinkable.keptChangeInterval *= 2;\n } else {\n const [largest] = bySizeDescending as [WindowCaptures, ...WindowCaptures[]];\n largest.frames = [];\n largest.isTooLarge = true;\n }\n }\n }\n}\n\nfunction isAnimated(captures: WindowCaptures): boolean {\n return captures.changeCount >= MIN_RECORDED_CHANGE_COUNT;\n}\n\n/**\n * Assembles PNG images into an animated PNG (APNG) that loops forever. The compressed image data of\n * each frame is reused as is, so the frames must share the pixel format of the first one; frames of\n * another format are left out. A frame smaller than the largest one is drawn at the top-left corner.\n */\nexport function encodeAnimatedPng(frames: readonly AnimationFrame[]): Buffer {\n const parsedFrames = frames.map((frame) => ({ ...parsePng(frame.png), delayMs: frame.delayMs }));\n const pixelFormat = parsedFrames[0]?.header.subarray(8);\n if (!pixelFormat) throw new Error('an animated PNG needs at least one frame');\n const animationFrames = parsedFrames\n .filter((frame) => frame.header.subarray(8).equals(pixelFormat))\n .map((frame) => ({ ...frame, width: frame.header.readUInt32BE(0), height: frame.header.readUInt32BE(4) }));\n const [firstFrame] = animationFrames as [(typeof animationFrames)[number], ...typeof animationFrames];\n const width = Math.max(...animationFrames.map((frame) => frame.width));\n const height = Math.max(...animationFrames.map((frame) => frame.height));\n\n const header = Buffer.from(firstFrame.header);\n header.writeUInt32BE(width, 0);\n header.writeUInt32BE(height, 4);\n const animationControl = Buffer.alloc(8);\n animationControl.writeUInt32BE(animationFrames.length, 0);\n const chunks = [\n PNG_SIGNATURE,\n createChunk('IHDR', header),\n ...firstFrame.metadataChunks,\n createChunk('acTL', animationControl),\n ];\n\n // The image shown by viewers without animation support must fill the canvas. Only a first frame\n // of that size can serve as it; otherwise it is a blank image that is not part of the animation.\n const startsWithFirstFrame = firstFrame.width === width && firstFrame.height === height;\n if (!startsWithFirstFrame) {\n const bitsPerPixel = (pixelFormat[0] as number) * (CHANNELS_BY_COLOR_TYPE[pixelFormat[1] as number] ?? 1);\n const blankScanlines = Buffer.alloc(height * (1 + Math.ceil((width * bitsPerPixel) / 8)));\n chunks.push(createChunk('IDAT', zlib.deflateSync(blankScanlines)));\n }\n\n let sequenceNumber = 0;\n for (const [index, frame] of animationFrames.entries()) {\n const frameControl = Buffer.alloc(26);\n frameControl.writeUInt32BE(sequenceNumber++, 0);\n frameControl.writeUInt32BE(frame.width, 4);\n frameControl.writeUInt32BE(frame.height, 8);\n // The x and y offsets at 12 and 16 stay 0.\n // The delay is a fraction of two 16-bit integers: milliseconds, or the finest unit a longer delay fits in.\n const delayUnitsPerSecond = Math.max(1, Math.min(1000, Math.floor((MAX_UINT16 * 1000) / frame.delayMs)));\n const delay = Math.round((frame.delayMs * delayUnitsPerSecond) / 1000);\n frameControl.writeUInt16BE(Math.min(Math.max(delay, 1), MAX_UINT16), 20);\n frameControl.writeUInt16BE(delayUnitsPerSecond, 22);\n // Every frame is cleared before the next one, so a larger frame does not stay visible around a smaller one.\n frameControl.writeUInt8(1, 24);\n chunks.push(createChunk('fcTL', frameControl));\n\n if (index === 0 && startsWithFirstFrame) {\n chunks.push(createChunk('IDAT', frame.imageData));\n } else {\n const sequence = Buffer.alloc(4);\n sequence.writeUInt32BE(sequenceNumber++);\n chunks.push(createChunk('fdAT', Buffer.concat([sequence, frame.imageData])));\n }\n }\n chunks.push(createChunk('IEND', Buffer.alloc(0)));\n return Buffer.concat(chunks);\n}\n\nfunction parsePng(png: Buffer): ParsedPng {\n if (!png.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error('not a PNG image');\n\n let header: Buffer | undefined;\n const metadataChunks: Buffer[] = [];\n const imageDataChunks: Buffer[] = [];\n let offset = PNG_SIGNATURE.length;\n while (offset + 12 <= png.length) {\n const dataLength = png.readUInt32BE(offset);\n const type = png.toString('latin1', offset + 4, offset + 8);\n const data = png.subarray(offset + 8, offset + 8 + dataLength);\n const nextOffset = offset + 12 + dataLength;\n if (type === 'IHDR') {\n header = data;\n } else if (type === 'IDAT') {\n imageDataChunks.push(data);\n } else if (imageDataChunks.length === 0 && type !== 'IEND') {\n metadataChunks.push(png.subarray(offset, nextOffset));\n }\n offset = nextOffset;\n }\n if (header?.length !== 13 || imageDataChunks.length === 0) throw new Error('not a PNG image');\n return { header, metadataChunks, imageData: Buffer.concat(imageDataChunks) };\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n const typeAndData = Buffer.concat([Buffer.from(type, 'latin1'), data]);\n const chunk = Buffer.alloc(typeAndData.length + 8);\n chunk.writeUInt32BE(data.length, 0);\n typeAndData.copy(chunk, 4);\n chunk.writeUInt32BE(zlib.crc32(typeAndData), chunk.length - 4);\n return chunk;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"guiRecording.js","names":["crypto","zlib","MAX_GUI_RECORDING_BYTES","PNG_SIGNATURE","CHANNELS_BY_COLOR_TYPE","MAX_UINT16","GuiRecorder","captures","#windowIdToCaptures","windowId","screenshot","capturedAtMs","digest","changeIndex","#shrinkToFit","frames","sum","frame","framesToAnimatedPng","encode","animatedPng","encodeAnimatedPng","index","lastSeenAtMs","frameIntervalMs","isRecorded","allCaptures","sizes","measure","size","bySizeDescending","b","a","notAnimated","isAnimated","shrinkable","largest","parsedFrames","parsePng","pixelFormat","animationFrames","firstFrame","width","height","header","animationControl","chunks","createChunk","startsWithFirstFrame","bitsPerPixel","blankScanlines","sequenceNumber","frameControl","delayUnitsPerSecond","delay","sequence","png","metadataChunks","imageDataChunks","offset","dataLength","type","data","nextOffset","typeAndData","chunk"],"sources":["../../src/helpers/guiRecording.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport zlib from 'node:zlib';\n\n/** Bounds the recordings of one GUI run in total, so a judge server can budget for them. */\nexport const MAX_GUI_RECORDING_BYTES = 2 * 1024 * 1024;\n\nexport interface GuiRecordingFile {\n path: string;\n data: string;\n encoding: 'base64';\n}\n\ninterface CapturedFrame {\n png: Buffer;\n capturedAtMs: number;\n}\n\ninterface WindowCaptures {\n path: string;\n /** The captures kept for the recording; none once the window turned out too large to record. */\n frames: CapturedFrame[];\n /** A digest of the last capture, kept or not, to tell whether the window changed. */\n lastDigest: string;\n changeCount: number;\n /** Every how many changes a capture is kept; doubled whenever half of the kept frames are dropped. */\n keptChangeInterval: number;\n lastSeenAtMs: number;\n isTooLarge: boolean;\n}\n\ninterface AnimationFrame {\n png: Buffer;\n delayMs: number;\n}\n\ninterface ParsedPng {\n /** The IHDR data: width, height, and the 5-byte pixel format. */\n header: Buffer;\n /** The chunks between IHDR and the image data (e.g. a palette), as they are in the file. */\n metadataChunks: Buffer[];\n imageData: Buffer;\n}\n\nconst PNG_SIGNATURE = Buffer.from('89504e470d0a1a0a', 'hex');\nconst CHANNELS_BY_COLOR_TYPE: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };\nconst MAX_UINT16 = 65_535;\n// A window is typically captured blank once before its first paint, which is not an animation.\nconst MIN_RECORDED_CHANGE_COUNT = 3;\n\n/**\n * Collects the screenshots of each window over a run and turns the windows that kept changing into\n * animated PNGs, which browsers play wherever they show a PNG. The captures it keeps are bounded\n * like the recordings, however long the run lasts and whatever the windows show.\n */\nexport class GuiRecorder {\n readonly #windowIdToCaptures = new Map<string, WindowCaptures>();\n\n add(windowId: string, screenshot: { path: string; data: string }, capturedAtMs: number): void {\n const captures = this.#windowIdToCaptures.get(windowId) ?? {\n path: '',\n frames: [],\n lastDigest: '',\n changeCount: 0,\n keptChangeInterval: 1,\n lastSeenAtMs: 0,\n isTooLarge: false,\n };\n this.#windowIdToCaptures.set(windowId, captures);\n captures.path = screenshot.path.replace(/\\.png$/, '_recording.png');\n captures.lastSeenAtMs = capturedAtMs;\n // An unchanged window only extends how long its last frame is shown.\n const digest = crypto.createHash('sha256').update(screenshot.data).digest('base64');\n if (captures.lastDigest === digest) return;\n\n captures.lastDigest = digest;\n const changeIndex = captures.changeCount++;\n if (captures.isTooLarge || changeIndex % captures.keptChangeInterval !== 0) return;\n\n captures.frames.push({ png: Buffer.from(screenshot.data, 'base64'), capturedAtMs });\n this.#shrinkToFit(({ frames }) => frames.reduce((sum, frame) => sum + frame.png.length, 0));\n }\n\n /** @param frameIntervalMs How long the last capture of a window is shown. */\n build(frameIntervalMs: number): GuiRecordingFile[] {\n const framesToAnimatedPng = new Map<CapturedFrame[], Buffer>();\n const encode = ({ frames, lastSeenAtMs }: WindowCaptures): Buffer => {\n const animatedPng =\n framesToAnimatedPng.get(frames) ??\n encodeAnimatedPng(\n frames.map((frame, index) => ({\n png: frame.png,\n delayMs: (frames[index + 1]?.capturedAtMs ?? lastSeenAtMs + frameIntervalMs) - frame.capturedAtMs,\n }))\n );\n framesToAnimatedPng.set(frames, animatedPng);\n return animatedPng;\n };\n // The chunks an animated PNG adds per frame can exceed what the frames alone were bounded to.\n this.#shrinkToFit((captures) => (isRecorded(captures) ? encode(captures).length : 0));\n return [...this.#windowIdToCaptures.values()].filter(isRecorded).map((captures) => ({\n path: captures.path,\n data: encode(captures).toString('base64'),\n encoding: 'base64',\n }));\n }\n\n /**\n * Frees space until all windows together fit `MAX_GUI_RECORDING_BYTES`, giving up what matters\n * least first: the early captures of a window that is not animated (yet), then every other frame\n * of the largest animated window, and only when no window has more than two frames a whole window,\n * which is then not recorded.\n */\n #shrinkToFit(measure: (captures: WindowCaptures) => number): void {\n const allCaptures = [...this.#windowIdToCaptures.values()];\n for (;;) {\n const sizes = new Map(allCaptures.map((captures) => [captures, measure(captures)]));\n if ([...sizes.values()].reduce((sum, size) => sum + size, 0) <= MAX_GUI_RECORDING_BYTES) return;\n\n const bySizeDescending = allCaptures\n .filter((captures) => captures.frames.length > 0)\n .toSorted((a, b) => (sizes.get(b) ?? 0) - (sizes.get(a) ?? 0));\n // The fewer changes a window showed, the less likely it is the start of an animation.\n const [notAnimated] = bySizeDescending\n .filter((captures) => !isAnimated(captures))\n .toSorted((a, b) => a.changeCount - b.changeCount);\n const shrinkable = bySizeDescending.find((captures) => captures.frames.length > 2);\n if (notAnimated) {\n notAnimated.frames = [];\n } else if (shrinkable) {\n shrinkable.frames = shrinkable.frames.filter((_, index) => index % 2 === 0);\n shrinkable.keptChangeInterval *= 2;\n } else {\n const [largest] = bySizeDescending as [WindowCaptures, ...WindowCaptures[]];\n largest.frames = [];\n largest.isTooLarge = true;\n }\n }\n }\n}\n\nfunction isRecorded(captures: WindowCaptures): boolean {\n return isAnimated(captures) && captures.frames.length > 1;\n}\n\nfunction isAnimated(captures: WindowCaptures): boolean {\n return captures.changeCount >= MIN_RECORDED_CHANGE_COUNT;\n}\n\n/**\n * Assembles PNG images into an animated PNG (APNG) that loops forever. The compressed image data of\n * each frame is reused as is, so the frames must share the pixel format of the first one; frames of\n * another format are left out. A frame smaller than the largest one is drawn at the top-left corner.\n */\nexport function encodeAnimatedPng(frames: readonly AnimationFrame[]): Buffer {\n const parsedFrames = frames.map((frame) => ({ ...parsePng(frame.png), delayMs: frame.delayMs }));\n const pixelFormat = parsedFrames[0]?.header.subarray(8);\n if (!pixelFormat) throw new Error('an animated PNG needs at least one frame');\n const animationFrames = parsedFrames\n .filter((frame) => frame.header.subarray(8).equals(pixelFormat))\n .map((frame) => ({ ...frame, width: frame.header.readUInt32BE(0), height: frame.header.readUInt32BE(4) }));\n const [firstFrame] = animationFrames as [(typeof animationFrames)[number], ...typeof animationFrames];\n const width = Math.max(...animationFrames.map((frame) => frame.width));\n const height = Math.max(...animationFrames.map((frame) => frame.height));\n\n const header = Buffer.from(firstFrame.header);\n header.writeUInt32BE(width, 0);\n header.writeUInt32BE(height, 4);\n const animationControl = Buffer.alloc(8);\n animationControl.writeUInt32BE(animationFrames.length, 0);\n const chunks = [\n PNG_SIGNATURE,\n createChunk('IHDR', header),\n ...firstFrame.metadataChunks,\n createChunk('acTL', animationControl),\n ];\n\n // The image shown by viewers without animation support must fill the canvas. Only a first frame\n // of that size can serve as it; otherwise it is a blank image that is not part of the animation.\n const startsWithFirstFrame = firstFrame.width === width && firstFrame.height === height;\n if (!startsWithFirstFrame) {\n const bitsPerPixel = (pixelFormat[0] as number) * (CHANNELS_BY_COLOR_TYPE[pixelFormat[1] as number] ?? 1);\n const blankScanlines = Buffer.alloc(height * (1 + Math.ceil((width * bitsPerPixel) / 8)));\n chunks.push(createChunk('IDAT', zlib.deflateSync(blankScanlines)));\n }\n\n let sequenceNumber = 0;\n for (const [index, frame] of animationFrames.entries()) {\n const frameControl = Buffer.alloc(26);\n frameControl.writeUInt32BE(sequenceNumber++, 0);\n frameControl.writeUInt32BE(frame.width, 4);\n frameControl.writeUInt32BE(frame.height, 8);\n // The x and y offsets at 12 and 16 stay 0.\n // The delay is a fraction of two 16-bit integers: milliseconds, or the finest unit a longer delay fits in.\n const delayUnitsPerSecond = Math.max(1, Math.min(1000, Math.floor((MAX_UINT16 * 1000) / frame.delayMs)));\n const delay = Math.round((frame.delayMs * delayUnitsPerSecond) / 1000);\n frameControl.writeUInt16BE(Math.min(Math.max(delay, 1), MAX_UINT16), 20);\n frameControl.writeUInt16BE(delayUnitsPerSecond, 22);\n // Every frame is cleared before the next one, so a larger frame does not stay visible around a smaller one.\n frameControl.writeUInt8(1, 24);\n chunks.push(createChunk('fcTL', frameControl));\n\n if (index === 0 && startsWithFirstFrame) {\n chunks.push(createChunk('IDAT', frame.imageData));\n } else {\n const sequence = Buffer.alloc(4);\n sequence.writeUInt32BE(sequenceNumber++);\n chunks.push(createChunk('fdAT', Buffer.concat([sequence, frame.imageData])));\n }\n }\n chunks.push(createChunk('IEND', Buffer.alloc(0)));\n return Buffer.concat(chunks);\n}\n\nfunction parsePng(png: Buffer): ParsedPng {\n if (!png.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error('not a PNG image');\n\n let header: Buffer | undefined;\n const metadataChunks: Buffer[] = [];\n const imageDataChunks: Buffer[] = [];\n let offset = PNG_SIGNATURE.length;\n while (offset + 12 <= png.length) {\n const dataLength = png.readUInt32BE(offset);\n const type = png.toString('latin1', offset + 4, offset + 8);\n const data = png.subarray(offset + 8, offset + 8 + dataLength);\n const nextOffset = offset + 12 + dataLength;\n if (type === 'IHDR') {\n header = data;\n } else if (type === 'IDAT') {\n imageDataChunks.push(data);\n } else if (imageDataChunks.length === 0 && type !== 'IEND') {\n metadataChunks.push(png.subarray(offset, nextOffset));\n }\n offset = nextOffset;\n }\n if (header?.length !== 13 || imageDataChunks.length === 0) throw new Error('not a PNG image');\n return { header, metadataChunks, imageData: Buffer.concat(imageDataChunks) };\n}\n\nfunction createChunk(type: string, data: Buffer): Buffer {\n const typeAndData = Buffer.concat([Buffer.from(type, 'latin1'), data]);\n const chunk = Buffer.alloc(typeAndData.length + 8);\n chunk.writeUInt32BE(data.length, 0);\n typeAndData.copy(chunk, 4);\n chunk.writeUInt32BE(zlib.crc32(typeAndData), chunk.length - 4);\n return chunk;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("zod");const i=5e4;async function a(e,r){let i=await t.default.promises.readFile(e,`utf8`),a=/^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(i);return a?.[1]?n.default.join(a[1].replaceAll(`.`,`/`),n.default.basename(r)):r}async function o(e){let t=[];return await s(e,``,t),t.toSorted()}async function s(e,r,i){let a=r?n.default.join(e,r):e,o=await t.default.promises.readdir(a,{withFileTypes:!0});for(let t of o){let a=r?n.default.join(r,t.name):t.name;if(t.isDirectory()){await s(e,a,i);continue}t.isFile()&&i.push(a)}}function c(e){if(!t.default.existsSync(e))return;let n=JSON.parse(t.default.readFileSync(e,`utf8`)),i=r.z.array(r.z.unknown()).safeParse(n);if(!i.success)return;let a=r.z.object({path:r.z.string(),data:r.z.string(),encoding:r.z.literal(`base64`).optional()});return i.data.flatMap(e=>{let t=a.safeParse(e);return t.success?[t.data]:[]})}function l(e){return e.slice(0,i)}exports.MAX_OUTPUT_LENGTH=i,exports.listSubmissionFiles=o,exports.readOutputFiles=c,exports.resolveJavaRelativePath=a,exports.truncateOutput=l;
1
+ "use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("zod");const i=5e4;async function a(e,r){let i=await t.default.promises.readFile(e,`utf8`),a=/^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(i);return a?.[1]?n.default.join(a[1].replaceAll(`.`,`/`),n.default.basename(r)):r}async function o(e){let t=[];return await s(e,``,t),t.toSorted()}async function s(e,r,i){let a=r?n.default.join(e,r):e,o=await t.default.promises.readdir(a,{withFileTypes:!0});for(let t of o){let a=r?n.default.join(r,t.name):t.name;t.isDirectory()?await s(e,a,i):t.isFile()&&i.push(a)}}function c(e){if(!t.default.existsSync(e))return;let n=JSON.parse(t.default.readFileSync(e,`utf8`)),i=r.z.array(r.z.unknown()).safeParse(n);if(!i.success)return;let a=r.z.object({path:r.z.string(),data:r.z.string(),encoding:r.z.literal(`base64`).optional()});return i.data.flatMap(e=>{let t=a.safeParse(e);return t.success?[t.data]:[]})}function l(e){return e.slice(0,i)}exports.MAX_OUTPUT_LENGTH=i,exports.listSubmissionFiles=o,exports.readOutputFiles=c,exports.resolveJavaRelativePath=a,exports.truncateOutput=l;
2
2
  //# sourceMappingURL=javaJudge.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"javaJudge.cjs","names":["fs","path","z","require_runtime","node_fs","node_path","zod","MAX_OUTPUT_LENGTH","resolveJavaRelativePath","sourceCode","sourcePath","packageMatch","fallbackRelativePath","listSubmissionFiles","filePaths","collectSubmissionFiles","rootDir","currentDir","relativeDir","entries","entry","entryRelativePath","readOutputFiles","filePath","parsed","values","outputFileSchema","result","value","truncateOutput"],"sources":["../../src/helpers/javaJudge.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { z } from 'zod';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\nexport const MAX_OUTPUT_LENGTH = 50_000;\n\nexport async function resolveJavaRelativePath(sourcePath: string, fallbackRelativePath: string): Promise<string> {\n const sourceCode = await fs.promises.readFile(sourcePath, 'utf8');\n const packageMatch = /^\\s*package\\s+([a-zA-Z0-9_.]+)\\s*;/m.exec(sourceCode);\n if (!packageMatch?.[1]) {\n return fallbackRelativePath;\n }\n\n return path.join(packageMatch[1].replaceAll('.', '/'), path.basename(fallbackRelativePath));\n}\n\nexport async function listSubmissionFiles(rootDir: string): Promise<string[]> {\n const filePaths: string[] = [];\n await collectSubmissionFiles(rootDir, '', filePaths);\n return filePaths.toSorted();\n}\n\nasync function collectSubmissionFiles(rootDir: string, relativeDir: string, filePaths: string[]): Promise<void> {\n const currentDir = relativeDir ? path.join(rootDir, relativeDir) : rootDir;\n const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });\n for (const entry of entries) {\n const entryRelativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;\n if (entry.isDirectory()) {\n await collectSubmissionFiles(rootDir, entryRelativePath, filePaths);\n continue;\n }\n if (entry.isFile()) {\n filePaths.push(entryRelativePath);\n }\n }\n}\n\nexport function readOutputFiles(filePath: string): OutputFile[] | undefined {\n if (!fs.existsSync(filePath)) {\n return undefined;\n }\n\n // Malformed evaluator JSON remains a harness error (JUDGE_NOT_AVAILABLE), not missing screenshots.\n const parsed: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n const values = z.array(z.unknown()).safeParse(parsed);\n if (!values.success) return undefined;\n const outputFileSchema = z.object({\n path: z.string(),\n data: z.string(),\n encoding: z.literal('base64').optional(),\n });\n return values.data.flatMap((value) => {\n const result = outputFileSchema.safeParse(value);\n return result.success ? [result.data] : [];\n });\n}\n\nexport function truncateOutput(value: string): string {\n return value.slice(0, MAX_OUTPUT_LENGTH);\n}\n"],"mappings":"6KAMA,MAAaO,EAAoB,IAEjC,eAAsBC,EAAwB,EAAoB,EAA+C,CAC/G,IAAMC,EAAa,MAAMT,EAAAA,QAAG,SAAS,SAASU,EAAY,MAAM,EAC1DC,EAAe,sCAAsC,KAAKF,CAAU,EAK1E,OAJKE,IAAe,GAIbV,EAAAA,QAAK,KAAKU,EAAa,EAAE,CAAC,WAAW,IAAK,GAAG,EAAGV,EAAAA,QAAK,SAASW,CAAoB,CAAC,EAHjFA,CAIX,CAEA,eAAsBC,EAAoB,EAAoC,CAC5E,IAAMC,EAAsB,CAAC,EAE7B,OADA,MAAMC,EAAuBC,EAAS,GAAIF,CAAS,EAC5CA,EAAU,SAAS,CAC5B,CAEA,eAAeC,EAAuB,EAAiB,EAAqB,EAAoC,CAC9G,IAAME,EAAaC,EAAcjB,EAAAA,QAAK,KAAKe,EAASE,CAAW,EAAIF,EAC7DG,EAAU,MAAMnB,EAAAA,QAAG,SAAS,QAAQiB,EAAY,CAAE,cAAe,EAAK,CAAC,EAC7E,IAAK,IAAMG,KAASD,EAAS,CAC3B,IAAME,EAAoBH,EAAcjB,EAAAA,QAAK,KAAKiB,EAAaE,EAAM,IAAI,EAAIA,EAAM,KACnF,GAAIA,EAAM,YAAY,EAAG,CACvB,MAAML,EAAuBC,EAASK,EAAmBP,CAAS,EAClE,QACF,CACIM,EAAM,OAAO,GACfN,EAAU,KAAKO,CAAiB,CAEpC,CACF,CAEA,SAAgBC,EAAgB,EAA4C,CAC1E,GAAI,CAACtB,EAAAA,QAAG,WAAWuB,CAAQ,EACzB,OAIF,IAAMC,EAAkB,KAAK,MAAMxB,EAAAA,QAAG,aAAauB,EAAU,MAAM,CAAC,EAC9DE,EAASvB,EAAAA,EAAE,MAAMA,EAAAA,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAUsB,CAAM,EACpD,GAAI,CAACC,EAAO,QAAS,OACrB,IAAMC,EAAmBxB,EAAAA,EAAE,OAAO,CAChC,KAAMA,EAAAA,EAAE,OAAO,EACf,KAAMA,EAAAA,EAAE,OAAO,EACf,SAAUA,EAAAA,EAAE,QAAQ,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EACD,OAAOuB,EAAO,KAAK,QAAS,GAAU,CACpC,IAAME,EAASD,EAAiB,UAAUE,CAAK,EAC/C,OAAOD,EAAO,QAAU,CAACA,EAAO,IAAI,EAAI,CAAC,CAC3C,CAAC,CACH,CAEA,SAAgBE,EAAe,EAAuB,CACpD,OAAOD,EAAM,MAAM,EAAGrB,CAAiB,CACzC"}
1
+ {"version":3,"file":"javaJudge.cjs","names":["fs","path","z","require_runtime","node_fs","node_path","zod","MAX_OUTPUT_LENGTH","resolveJavaRelativePath","sourceCode","sourcePath","packageMatch","fallbackRelativePath","listSubmissionFiles","filePaths","collectSubmissionFiles","rootDir","currentDir","relativeDir","entries","entry","entryRelativePath","readOutputFiles","filePath","parsed","values","outputFileSchema","result","value","truncateOutput"],"sources":["../../src/helpers/javaJudge.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { z } from 'zod';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\nexport const MAX_OUTPUT_LENGTH = 50_000;\n\nexport async function resolveJavaRelativePath(sourcePath: string, fallbackRelativePath: string): Promise<string> {\n const sourceCode = await fs.promises.readFile(sourcePath, 'utf8');\n const packageMatch = /^\\s*package\\s+([a-zA-Z0-9_.]+)\\s*;/m.exec(sourceCode);\n if (!packageMatch?.[1]) {\n return fallbackRelativePath;\n }\n\n return path.join(packageMatch[1].replaceAll('.', '/'), path.basename(fallbackRelativePath));\n}\n\nexport async function listSubmissionFiles(rootDir: string): Promise<string[]> {\n const filePaths: string[] = [];\n await collectSubmissionFiles(rootDir, '', filePaths);\n return filePaths.toSorted();\n}\n\nasync function collectSubmissionFiles(rootDir: string, relativeDir: string, filePaths: string[]): Promise<void> {\n const currentDir = relativeDir ? path.join(rootDir, relativeDir) : rootDir;\n const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });\n for (const entry of entries) {\n const entryRelativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;\n if (entry.isDirectory()) {\n await collectSubmissionFiles(rootDir, entryRelativePath, filePaths);\n continue;\n }\n if (entry.isFile()) {\n filePaths.push(entryRelativePath);\n }\n }\n}\n\nexport function readOutputFiles(filePath: string): OutputFile[] | undefined {\n if (!fs.existsSync(filePath)) {\n return undefined;\n }\n\n // Malformed evaluator JSON remains a harness error (JUDGE_NOT_AVAILABLE), not missing screenshots.\n const parsed: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n const values = z.array(z.unknown()).safeParse(parsed);\n if (!values.success) return undefined;\n const outputFileSchema = z.object({\n path: z.string(),\n data: z.string(),\n encoding: z.literal('base64').optional(),\n });\n return values.data.flatMap((value) => {\n const result = outputFileSchema.safeParse(value);\n return result.success ? [result.data] : [];\n });\n}\n\nexport function truncateOutput(value: string): string {\n return value.slice(0, MAX_OUTPUT_LENGTH);\n}\n"],"mappings":"6KAMA,MAAaO,EAAoB,IAEjC,eAAsBC,EAAwB,EAAoB,EAA+C,CAC/G,IAAMC,EAAa,MAAMT,EAAAA,QAAG,SAAS,SAASU,EAAY,MAAM,EAC1DC,EAAe,sCAAsC,KAAKF,CAAU,EAK1E,OAJKE,IAAe,GAIbV,EAAAA,QAAK,KAAKU,EAAa,EAAE,CAAC,WAAW,IAAK,GAAG,EAAGV,EAAAA,QAAK,SAASW,CAAoB,CAAC,EAHjFA,CAIX,CAEA,eAAsBC,EAAoB,EAAoC,CAC5E,IAAMC,EAAsB,CAAC,EAE7B,OADA,MAAMC,EAAuBC,EAAS,GAAIF,CAAS,EAC5CA,EAAU,SAAS,CAC5B,CAEA,eAAeC,EAAuB,EAAiB,EAAqB,EAAoC,CAC9G,IAAME,EAAaC,EAAcjB,EAAAA,QAAK,KAAKe,EAASE,CAAW,EAAIF,EAC7DG,EAAU,MAAMnB,EAAAA,QAAG,SAAS,QAAQiB,EAAY,CAAE,cAAe,EAAK,CAAC,EAC7E,IAAK,IAAMG,KAASD,EAAS,CAC3B,IAAME,EAAoBH,EAAcjB,EAAAA,QAAK,KAAKiB,EAAaE,EAAM,IAAI,EAAIA,EAAM,KAC/EA,EAAM,YAAY,EACpB,MAAML,EAAuBC,EAASK,EAAmBP,CAAS,EAGhEM,EAAM,OAAO,GACfN,EAAU,KAAKO,CAAiB,CAEpC,CACF,CAEA,SAAgBC,EAAgB,EAA4C,CAC1E,GAAI,CAACtB,EAAAA,QAAG,WAAWuB,CAAQ,EACzB,OAIF,IAAMC,EAAkB,KAAK,MAAMxB,EAAAA,QAAG,aAAauB,EAAU,MAAM,CAAC,EAC9DE,EAASvB,EAAAA,EAAE,MAAMA,EAAAA,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAUsB,CAAM,EACpD,GAAI,CAACC,EAAO,QAAS,OACrB,IAAMC,EAAmBxB,EAAAA,EAAE,OAAO,CAChC,KAAMA,EAAAA,EAAE,OAAO,EACf,KAAMA,EAAAA,EAAE,OAAO,EACf,SAAUA,EAAAA,EAAE,QAAQ,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EACD,OAAOuB,EAAO,KAAK,QAAS,GAAU,CACpC,IAAME,EAASD,EAAiB,UAAUE,CAAK,EAC/C,OAAOD,EAAO,QAAU,CAACA,EAAO,IAAI,EAAI,CAAC,CAC3C,CAAC,CACH,CAEA,SAAgBE,EAAe,EAAuB,CACpD,OAAOD,EAAM,MAAM,EAAGrB,CAAiB,CACzC"}
@@ -1,2 +1,2 @@
1
- import e from"node:fs";import t from"node:path";import{z as n}from"zod";const r=5e4;async function i(n,r){let i=await e.promises.readFile(n,`utf8`),a=/^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(i);return a?.[1]?t.join(a[1].replaceAll(`.`,`/`),t.basename(r)):r}async function a(e){let t=[];return await o(e,``,t),t.toSorted()}async function o(n,r,i){let a=r?t.join(n,r):n,s=await e.promises.readdir(a,{withFileTypes:!0});for(let e of s){let a=r?t.join(r,e.name):e.name;if(e.isDirectory()){await o(n,a,i);continue}e.isFile()&&i.push(a)}}function s(t){if(!e.existsSync(t))return;let r=JSON.parse(e.readFileSync(t,`utf8`)),i=n.array(n.unknown()).safeParse(r);if(!i.success)return;let a=n.object({path:n.string(),data:n.string(),encoding:n.literal(`base64`).optional()});return i.data.flatMap(e=>{let t=a.safeParse(e);return t.success?[t.data]:[]})}function c(e){return e.slice(0,r)}export{r as MAX_OUTPUT_LENGTH,a as listSubmissionFiles,s as readOutputFiles,i as resolveJavaRelativePath,c as truncateOutput};
1
+ import e from"node:fs";import t from"node:path";import{z as n}from"zod";const r=5e4;async function i(n,r){let i=await e.promises.readFile(n,`utf8`),a=/^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(i);return a?.[1]?t.join(a[1].replaceAll(`.`,`/`),t.basename(r)):r}async function a(e){let t=[];return await o(e,``,t),t.toSorted()}async function o(n,r,i){let a=r?t.join(n,r):n,s=await e.promises.readdir(a,{withFileTypes:!0});for(let e of s){let a=r?t.join(r,e.name):e.name;e.isDirectory()?await o(n,a,i):e.isFile()&&i.push(a)}}function s(t){if(!e.existsSync(t))return;let r=JSON.parse(e.readFileSync(t,`utf8`)),i=n.array(n.unknown()).safeParse(r);if(!i.success)return;let a=n.object({path:n.string(),data:n.string(),encoding:n.literal(`base64`).optional()});return i.data.flatMap(e=>{let t=a.safeParse(e);return t.success?[t.data]:[]})}function c(e){return e.slice(0,r)}export{r as MAX_OUTPUT_LENGTH,a as listSubmissionFiles,s as readOutputFiles,i as resolveJavaRelativePath,c as truncateOutput};
2
2
  //# sourceMappingURL=javaJudge.js.map