@exercode/problem-utils 3.2.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,88 +1,7 @@
1
1
  # @exercode/problem-utils
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@exercode/problem-utils.svg)](https://www.npmjs.com/package/@exercode/problem-utils)
4
- [![Test](https://github.com/WillBooster/exercode-problem-utils/actions/workflows/test.yml/badge.svg)](https://github.com/WillBooster/exercode-problem-utils/actions/workflows/test.yml)
5
- [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
6
- [![wbfy](https://img.shields.io/badge/wbfy-20.10.0-1e90ff.svg)](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
3
+ CLI, validation, result types, and stdio/command/GUI/evaluation judging presets for Exercode.
4
+ This package does not depend on a browser library or AI SDK providers.
7
5
 
8
- :100: A set of utilities for judging programs on Exercode (https://exercode.willbooster.com/).
9
-
10
- ## CLI
11
-
12
- The package ships an `exercode-problem` command for problem authors (run it with `bun x` in a repository that depends on `@exercode/problem-utils`):
13
-
14
- ```bash
15
- # Judge all model answers of all problems (directories containing problem.md or <id>.problem.md) under a directory.
16
- # model_answers/* must be fully accepted; model_answers.fails/* must fail at least one test case.
17
- bun x exercode-problem # everything under the current directory
18
- bun x exercode-problem courses/foo # everything under courses/foo
19
- bun x exercode-problem --only a_plus --skip gui_ --concurrency 2
20
-
21
- # Judge one answer directory of the problem in the current directory.
22
- bun x exercode-problem judge model_answers/python
23
- bun x exercode-problem judge model_answers/python '{ "language": "python" }'
24
-
25
- # Debug one answer directory of the problem in the current directory.
26
- bun x exercode-problem debug model_answers/python '{ "stdin": "1 2" }'
27
- ```
28
-
29
- `judge` and `debug` run a custom `judge.ts` / `debug.ts` when the problem has one, and apply `stdioJudgePreset` / `stdioDebugPreset` otherwise, mirroring the Exercode server. The debug fallback applies only to standard problems: a problem with a custom `judge.ts` needs its own `debug.ts`, and `exercode-problem debug` fails with a message otherwise (the server likewise reports debug as unsupported there).
30
-
31
- The all-problem check judges serially by default because time limits are measured in wall-clock time; pass `--concurrency <n>` to parallelize when the checked problems are not timing-sensitive.
32
-
33
- A standard stdin/stdout problem must NOT commit a `judge.ts` or `debug.ts` that is identical to the default stdio harness: the absence of `judge.ts` marks the problem as standard, and committed copies would drift from the server's defaults. The CLI rejects such files; a file kept intentionally (e.g. to demonstrate the default harness) can add an explanatory comment to be treated as custom.
34
-
35
- ## Validators
36
-
37
- The CLI also validates learning-material files without running any program, mirroring the checks the Exercode importer applies:
38
-
39
- ```bash
40
- # Validate problem directories (problem.md frontmatter, test_cases, model_answers, templates, judge.ts / debug.ts).
41
- bun x exercode-problem validate-problem <problemDir>...
42
- # Validate a course directory (course.yaml, lecture materials with embedded questions, problem references).
43
- bun x exercode-problem validate-course <courseDir> [--problems-dir <dir>]
44
- # Validate a contest (*.contest.yaml) file.
45
- bun x exercode-problem validate-contest <contestYamlPath> [--problems-dir <dir>]
46
- ```
47
-
48
- Each target prints `OK` or `NG` followed by its errors and warnings; the command exits 1 when any target has an error. `--problems-dir` points to the directory holding the referenced problems; a course is always searched at any depth, since Exercode links a material only to the problems inside its course, so for a course the option must name a directory inside it. The validators are also exported (`validateProblemDirectory`, `validateCourseDirectory`, `validateContestFile`, `validateMaterialFile`).
49
-
50
- ## Agent skills
51
-
52
- The [`skills/`](skills/) directory holds skills for AI coding agents that author and review Exercode learning content: `generate-learning-content` (entry point), `generate-course-materials`, `generate-judge-problems`, `generate-judge-contest`, `review-learning-content`, and `setup-exercode-course-repository`. Agents working in this repository load them through the symlinks under `.claude/skills/`; install them elsewhere with the [skills](https://github.com/vercel-labs/skills) CLI:
53
-
54
- ```bash
55
- bun x skills add WillBooster/exercode-problem-utils --agent claude-code --agent codex
56
- ```
57
-
58
- ## Test cases
59
-
60
- A problem keeps its test cases under `test_cases/`. A test case id is the shared name of the following entries, and each entry is optional:
61
-
62
- | Entry | Meaning |
63
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
64
- | `<id>.in` | Standard input. Omit it (or leave it empty) when the program reads nothing. |
65
- | `<id>.out` | Expected standard output. |
66
- | `<id>.fin/` | Files copied into the working directory before the run (input files). |
67
- | `<id>.fout/` | Expected output files, compared with the files of the same relative paths in the working directory. |
68
- | `_shared.fin/` | Files copied into the working directory before every test case. |
69
- | `<id>.json` | Configuration for a custom `judge.ts` that reads it itself; the presets ignore it (the Judge server lists it as a test case of the custom judge). |
70
-
71
- A test case whose id contains `example` (the judge server's rule, e.g. `example_1` or `01_example_small`) is an example shown to learners; every other case is hidden.
72
-
73
- Standard output and text files are compared as space-separated tokens: consecutive white spaces count as one separator, and an expected token that contains a decimal point and parses as a finite number (e.g. `3.14`, but not `1`, `1e-3` or `1.0e309`) accepts a value within an absolute or relative error of `1e-6`. A file is text when it is valid UTF-8 without NUL bytes; other files (e.g. images) must match byte for byte. A received file larger than 8 MiB counts as not produced, an expected file larger than 8 MiB is an authoring error (the case is reported as a runtime error), and a file larger than 1 MiB is left out of the reported pair. When a file differs, the result carries `<name>_expected.<ext>` and `<name>_received.<ext>` so Exercode can show both (Exercode decides per test case whether a learner may see them, as it does for expected stdout).
74
-
75
- How a missing expectation is treated depends on the harness:
76
-
77
- - `stdioJudgePreset` (the default for problems without `judge.ts`) requires `<id>.out` or a non-empty `<id>.fout/` for every test case, so a standard problem cannot accept a run without checking it. A problem whose `problem.md` declares `requiredOutputFilePaths` or `isManualScoringRequired` is exempt, because every test case is judged by those instead; code rules (`requiredRegExpsInCode` etc.) and `requiredSubmissionFilePaths` check the submission once, in addition to the output comparison, and do not exempt.
78
- - `commandJudgePreset` without a `test` option checks whatever expectations exist, and a test case with neither only has to run within the limits. A `test` option replaces that comparison; it receives `testCase.output`, `testCase.fileOutputPath` and `cwd` and can call the exported `judgeAgainstExpectations` (or `compareStdoutAsSpaceSeparatedTokens` and `compareExpectedOutputFiles`). A custom `readTestCases` may return any test case type with `id` (plus optional `input` and `fileInputPath`); the default verdict judges any case that exposes a string `output` or `fileOutputPath`, which the default reader's `CommandTestCase` does.
79
- - `guiCommandJudgePreset` passes the expectations to the problem's `test`, which decides everything.
80
- - `llmJudgePreset` runs no program, so it copies no `.fin/`; it hands `<id>.in` as the prompt input and the whole entry (including `fileOutputPath`) to the problem's `test`.
81
- - `stdioDebugPreset` by default copies the answer directory to a temporary directory and builds and runs it there together with `_shared.fin/` and the first example case's `.fin/` (the case's files win; hidden cases' inputs are never handed to learner code), so the answer directory is left untouched (its `node_modules`, and those of its ancestors, are linked into the copy rather than duplicated); files the program writes are reported only through `requiredOutputFilePaths`. Callers that already own a disposable answer directory can pass `{ disposableWorkingDirectory: true }` as the second argument to build and run directly there; they must remove it themselves, including when the harness is terminated.
82
- - `evaluationJudgePreset` does not use `test_cases/`.
83
-
84
- `readTestCases` is exported for harnesses that enumerate `test_cases/` themselves.
85
-
86
- ## Measurements
87
-
88
- `stdioJudgePreset`, `stdioDebugPreset`, and `commandJudgePreset` (with its default runner; a custom `runCommand` decides whether its results carry `cpuTimeSeconds`) run a program under GNU time (`/usr/bin/time` on Linux, `gtime` on macOS) and report its wall time (`timeSeconds`), user plus system CPU time (`cpuTimeSeconds`), and peak resident set size (`memoryBytes`, at least the footprint of the GNU `timeout` wrapper the program runs under, about 1 MiB) in every test case result; `guiCommandJudgePreset` reports the wall time and the peak resident set size, and `llmJudgePreset` the wall time only. Time limits are judged by wall time. The CPU time is recorded even for a run that exceeded its limit, so a judge server sharing CPUs between programs can tell a program that used up its limit (its CPU time, summed over its threads, reaches the limit, so it would exceed it on any single CPU) from one that may only have waited for a CPU (its CPU time stays below the limit) and re-run just the latter alone.
6
+ See the [repository documentation](https://github.com/WillBooster/exercode-problem-utils)
7
+ for usage, problem formats, and the separate browser and LLM packages.
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./temporaryProblemDirCopy.cjs");let n=require("node:child_process");n=e.__toESM(n,1);const r=new Set;let i=!1;function a(e,t){return o(),new Promise(i=>{let a=n.default.spawn(process.execPath,e,{cwd:t.cwd,detached:process.platform!==`win32`,env:t.env,stdio:[`ignore`,`pipe`,`pipe`]}),o=a.pid===void 0?void 0:{pid:a.pid,tempRoot:t.tempRoot};o&&r.add(o);let c=[],l=[],u=0,d,f=!1,p=!1,m=e=>{if(d!==void 0)return!1;d=e,a.stdout?.destroy(),a.stderr?.destroy();try{a.pid===void 0?a.kill(`SIGKILL`):s(a.pid)}catch{a.kill(`SIGKILL`)}return!0},h=setTimeout(()=>{f=m(`timed out after ${t.timeoutMs/1e3} seconds`)},t.timeoutMs),g=(e,n)=>{if(u+=e.byteLength,u>t.maxOutputBytes){m(`the harness printed more than ${t.maxOutputBytes/1024/1024} MB of output`);return}n.push(e)};a.stdout?.on(`data`,e=>g(e,c)),a.stderr?.on(`data`,e=>g(e,l));let _=(e,t,n)=>{p||(p=!0,clearTimeout(h),o&&r.delete(o),i({stdout:Buffer.concat(c).toString(`utf8`),stderr:Buffer.concat(l).toString(`utf8`),exitCode:e,signal:t,failureReason:d??(n?`failed to run the harness: ${n.message}`:void 0),timedOut:f}))};a.on(`error`,e=>_(void 0,void 0,e)),a.on(`close`,(e,t)=>_(e??void 0,t??void 0))})}function o(){if(!i){i=!0;for(let e of[`SIGINT`,`SIGTERM`])process.once(e,()=>{for(let e of r){try{s(e.pid)}catch{}e.tempRoot&&t.forciblyRemoveDirectorySync(e.tempRoot)}process.kill(process.pid,e)})}}function s(e){process.platform===`win32`?n.default.spawnSync(`taskkill`,[`/PID`,String(e),`/T`,`/F`],{stdio:`ignore`}):process.kill(-e,`SIGKILL`)}exports.runHarnessProcess=a;
1
+ "use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./temporaryProblemDirCopy.cjs");let n=require("node:child_process");n=e.__toESM(n,1);const r=new Set;let i=!1;function a(e,t){return o(),new Promise(i=>{let a=n.default.spawn(process.execPath,e,{cwd:t.cwd,detached:process.platform!==`win32`,env:t.tempRoot?{...t.env,TMPDIR:t.tempRoot}:t.env,stdio:[`ignore`,`pipe`,`pipe`]}),o=a.pid===void 0?void 0:{pid:a.pid,tempRoot:t.tempRoot};o&&r.add(o);let c=[],l=[],u=0,d,f=!1,p=!1,m=e=>{if(d!==void 0)return!1;d=e,a.stdout?.destroy(),a.stderr?.destroy();try{a.pid===void 0?a.kill(`SIGKILL`):s(a.pid)}catch{a.kill(`SIGKILL`)}return!0},h=setTimeout(()=>{f=m(`timed out after ${t.timeoutMs/1e3} seconds`)},t.timeoutMs),g=(e,n)=>{if(u+=e.byteLength,u>t.maxOutputBytes){m(`the harness printed more than ${t.maxOutputBytes/1024/1024} MB of output`);return}n.push(e)};a.stdout?.on(`data`,e=>g(e,c)),a.stderr?.on(`data`,e=>g(e,l));let _=(e,t,n)=>{p||(p=!0,clearTimeout(h),o&&r.delete(o),i({stdout:Buffer.concat(c).toString(`utf8`),stderr:Buffer.concat(l).toString(`utf8`),exitCode:e,signal:t,failureReason:d??(n?`failed to run the harness: ${n.message}`:void 0),timedOut:f}))};a.on(`error`,e=>_(void 0,void 0,e)),a.on(`close`,(e,t)=>_(e??void 0,t??void 0))})}function o(){if(!i){i=!0;for(let e of[`SIGINT`,`SIGTERM`])process.once(e,()=>{for(let e of r){try{s(e.pid)}catch{}e.tempRoot&&t.forciblyRemoveDirectorySync(e.tempRoot)}process.kill(process.pid,e)})}}function s(e){process.platform===`win32`?n.default.spawnSync(`taskkill`,[`/PID`,String(e),`/T`,`/F`],{stdio:`ignore`}):process.kill(-e,`SIGKILL`)}exports.runHarnessProcess=a;
2
2
  //# sourceMappingURL=runHarnessProcess.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"runHarnessProcess.cjs","names":["child_process"],"sources":["../../src/helpers/runHarnessProcess.ts"],"sourcesContent":["import child_process from 'node:child_process';\n\nimport { forciblyRemoveDirectorySync } from './temporaryProblemDirCopy.js';\n\nexport interface HarnessProcessOptions {\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeoutMs: number;\n maxOutputBytes: number;\n /** A temporary copy to remove when the check itself is interrupted by a signal. */\n tempRoot?: string;\n}\n\nexport interface HarnessProcessResult {\n stdout: string;\n stderr: string;\n exitCode: number | undefined;\n /** The signal that terminated the harness when it did not exit on its own. */\n signal: NodeJS.Signals | undefined;\n /** Set when the run was killed by this helper (timeout or output cap) or could not be spawned. */\n failureReason: string | undefined;\n timedOut: boolean;\n}\n\ninterface LiveHarnessRun {\n pid: number;\n tempRoot: string | undefined;\n}\n\n// Detached harness groups no longer receive the terminal's SIGINT, so an interrupted run must\n// tear them down (and remove their temp copies) itself before exiting. The handlers stay installed\n// for the process lifetime: once the last run has settled they only iterate an empty set and re-raise\n// the signal, so removing them would change nothing observable.\nconst liveHarnessRuns = new Set<LiveHarnessRun>();\nlet signalHandlersInstalled = false;\n\n/**\n * Run a judge harness with `process.execPath` in its own process group, killing the whole group on\n * timeout or when the output cap is exceeded. The submissions a harness judges through\n * `spawnWithLimits` run in sessions of their own, out of that kill's reach; each wrapper's process\n * group is bounded by its own GNU `timeout` instead, while a descendant that moved to yet another\n * session may survive without stalling the harness.\n */\nexport function runHarnessProcess(\n commandArgs: readonly string[],\n options: HarnessProcessOptions\n): Promise<HarnessProcessResult> {\n installSignalHandlers();\n return new Promise((resolve) => {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const child = child_process.spawn(process.execPath, commandArgs, {\n cwd: options.cwd,\n detached: process.platform !== 'win32',\n env: options.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const liveRun: LiveHarnessRun | undefined =\n child.pid === undefined ? undefined : { pid: child.pid, tempRoot: options.tempRoot };\n if (liveRun) liveHarnessRuns.add(liveRun);\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let totalOutputBytes = 0;\n let failureReason: string | undefined;\n let timedOut = false;\n let settled = false;\n\n // Returns false when an earlier kill already recorded its reason, so callers can tell whether theirs won.\n const killProcessGroup = (reason: string): boolean => {\n if (failureReason !== undefined) return false;\n failureReason = reason;\n // Stop buffering immediately: OS pipe buffers can keep emitting data after the kill, and\n // destroyed streams also let `close` fire even if a stray grandchild inherited the pipes.\n child.stdout?.destroy();\n child.stderr?.destroy();\n try {\n if (child.pid === undefined) {\n child.kill('SIGKILL');\n } else {\n killHarnessTree(child.pid);\n }\n } catch {\n child.kill('SIGKILL');\n }\n return true;\n };\n const timeoutId = setTimeout(() => {\n timedOut = killProcessGroup(`timed out after ${options.timeoutMs / 1000} seconds`);\n }, options.timeoutMs);\n\n const appendOutput = (chunk: Buffer, chunks: Buffer[]): void => {\n totalOutputBytes += chunk.byteLength;\n if (totalOutputBytes > options.maxOutputBytes) {\n killProcessGroup(`the harness printed more than ${options.maxOutputBytes / 1024 / 1024} MB of output`);\n return;\n }\n chunks.push(chunk);\n };\n child.stdout?.on('data', (chunk: Buffer) => appendOutput(chunk, stdoutChunks));\n child.stderr?.on('data', (chunk: Buffer) => appendOutput(chunk, stderrChunks));\n\n const settle = (exitCode: number | undefined, signal: NodeJS.Signals | undefined, spawnError?: Error): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n if (liveRun) liveHarnessRuns.delete(liveRun);\n resolve({\n stdout: Buffer.concat(stdoutChunks).toString('utf8'),\n stderr: Buffer.concat(stderrChunks).toString('utf8'),\n exitCode,\n signal,\n failureReason: failureReason ?? (spawnError ? `failed to run the harness: ${spawnError.message}` : undefined),\n timedOut,\n });\n };\n child.on('error', (error) => settle(undefined, undefined, error));\n child.on('close', (exitCode, signal) => settle(exitCode ?? undefined, signal ?? undefined));\n });\n}\n\nfunction installSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, () => {\n for (const liveRun of liveHarnessRuns) {\n try {\n killHarnessTree(liveRun.pid);\n } catch {\n // The tree already exited.\n }\n // Best-effort cleanup while exiting; judged code may have left permission-locked entries.\n if (liveRun.tempRoot) forciblyRemoveDirectorySync(liveRun.tempRoot);\n }\n process.kill(process.pid, signal);\n });\n }\n}\n\n// On Windows the harness is not detached (process groups are unavailable), so killing only the\n// direct child would leave submission grandchildren running; taskkill terminates the whole tree.\nfunction killHarnessTree(pid: number): void {\n if (process.platform === 'win32') {\n child_process.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });\n } else {\n process.kill(-pid, 'SIGKILL');\n }\n}\n"],"mappings":"kKAiCA,MAAM,EAAkB,IAAI,IAC5B,IAAI,EAA0B,GAS9B,SAAgB,EACd,EACA,EAC+B,CAE/B,OADA,EAAsB,EACf,IAAI,QAAS,GAAY,CAE9B,IAAM,EAAQA,EAAAA,QAAc,MAAM,QAAQ,SAAU,EAAa,CAC/D,IAAK,EAAQ,IACb,SAAU,QAAQ,WAAa,QAC/B,IAAK,EAAQ,IACb,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EACK,EACJ,EAAM,MAAQ,IAAA,GAAY,IAAA,GAAY,CAAE,IAAK,EAAM,IAAK,SAAU,EAAQ,QAAS,EACjF,GAAS,EAAgB,IAAI,CAAO,EACxC,IAAM,EAAyB,CAAC,EAC1B,EAAyB,CAAC,EAC5B,EAAmB,EACnB,EACA,EAAW,GACX,EAAU,GAGR,EAAoB,GAA4B,CACpD,GAAI,IAAkB,IAAA,GAAW,MAAO,GACxC,EAAgB,EAGhB,EAAM,QAAQ,QAAQ,EACtB,EAAM,QAAQ,QAAQ,EACtB,GAAI,CACE,EAAM,MAAQ,IAAA,GAChB,EAAM,KAAK,SAAS,EAEpB,EAAgB,EAAM,GAAG,CAE7B,MAAQ,CACN,EAAM,KAAK,SAAS,CACtB,CACA,MAAO,EACT,EACM,EAAY,eAAiB,CACjC,EAAW,EAAiB,mBAAmB,EAAQ,UAAY,IAAK,SAAS,CACnF,EAAG,EAAQ,SAAS,EAEd,GAAgB,EAAe,IAA2B,CAE9D,GADA,GAAoB,EAAM,WACtB,EAAmB,EAAQ,eAAgB,CAC7C,EAAiB,iCAAiC,EAAQ,eAAiB,KAAO,KAAK,cAAc,EACrG,MACF,CACA,EAAO,KAAK,CAAK,CACnB,EACA,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAC7E,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAE7E,IAAM,GAAU,EAA8B,EAAoC,IAA6B,CACzG,IACJ,EAAU,GACV,aAAa,CAAS,EAClB,GAAS,EAAgB,OAAO,CAAO,EAC3C,EAAQ,CACN,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,WACA,SACA,cAAe,IAAkB,EAAa,8BAA8B,EAAW,UAAY,IAAA,IACnG,UACF,CAAC,EACH,EACA,EAAM,GAAG,QAAU,GAAU,EAAO,IAAA,GAAW,IAAA,GAAW,CAAK,CAAC,EAChE,EAAM,GAAG,SAAU,EAAU,IAAW,EAAO,GAAY,IAAA,GAAW,GAAU,IAAA,EAAS,CAAC,CAC5F,CAAC,CACH,CAEA,SAAS,GAA8B,CACjC,MACJ,GAA0B,GAC1B,IAAK,IAAM,IAAU,CAAC,SAAU,SAAS,EACvC,QAAQ,KAAK,MAAc,CACzB,IAAK,IAAM,KAAW,EAAiB,CACrC,GAAI,CACF,EAAgB,EAAQ,GAAG,CAC7B,MAAQ,CAER,CAEI,EAAQ,UAAU,EAAA,4BAA4B,EAAQ,QAAQ,CACpE,CACA,QAAQ,KAAK,QAAQ,IAAK,CAAM,CAClC,CAAC,CAbuB,CAe5B,CAIA,SAAS,EAAgB,EAAmB,CACtC,QAAQ,WAAa,QACvB,EAAA,QAAc,UAAU,WAAY,CAAC,OAAQ,OAAO,CAAG,EAAG,KAAM,IAAI,EAAG,CAAE,MAAO,QAAS,CAAC,EAE1F,QAAQ,KAAK,CAAC,EAAK,SAAS,CAEhC"}
1
+ {"version":3,"file":"runHarnessProcess.cjs","names":["child_process"],"sources":["../../src/helpers/runHarnessProcess.ts"],"sourcesContent":["import child_process from 'node:child_process';\n\nimport { forciblyRemoveDirectorySync } from './temporaryProblemDirCopy.js';\n\nexport interface HarnessProcessOptions {\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeoutMs: number;\n maxOutputBytes: number;\n /** A temporary copy to remove when the check itself is interrupted by a signal. */\n tempRoot?: string;\n}\n\nexport interface HarnessProcessResult {\n stdout: string;\n stderr: string;\n exitCode: number | undefined;\n /** The signal that terminated the harness when it did not exit on its own. */\n signal: NodeJS.Signals | undefined;\n /** Set when the run was killed by this helper (timeout or output cap) or could not be spawned. */\n failureReason: string | undefined;\n timedOut: boolean;\n}\n\ninterface LiveHarnessRun {\n pid: number;\n tempRoot: string | undefined;\n}\n\n// Detached harness groups no longer receive the terminal's SIGINT, so an interrupted run must\n// tear them down (and remove their temp copies) itself before exiting. The handlers stay installed\n// for the process lifetime: once the last run has settled they only iterate an empty set and re-raise\n// the signal, so removing them would change nothing observable.\nconst liveHarnessRuns = new Set<LiveHarnessRun>();\nlet signalHandlersInstalled = false;\n\n/**\n * Run a judge harness with `process.execPath` in its own process group, killing the whole group on\n * timeout or when the output cap is exceeded. The submissions a harness judges through\n * `spawnWithLimits` run in sessions of their own, out of that kill's reach; each wrapper's process\n * group is bounded by its own GNU `timeout` instead, while a descendant that moved to yet another\n * session may survive without stalling the harness.\n */\nexport function runHarnessProcess(\n commandArgs: readonly string[],\n options: HarnessProcessOptions\n): Promise<HarnessProcessResult> {\n installSignalHandlers();\n return new Promise((resolve) => {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const child = child_process.spawn(process.execPath, commandArgs, {\n cwd: options.cwd,\n detached: process.platform !== 'win32',\n // The parent removes this root even when SIGKILL prevents child cleanup.\n env: options.tempRoot ? { ...options.env, TMPDIR: options.tempRoot } : options.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const liveRun: LiveHarnessRun | undefined =\n child.pid === undefined ? undefined : { pid: child.pid, tempRoot: options.tempRoot };\n if (liveRun) liveHarnessRuns.add(liveRun);\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let totalOutputBytes = 0;\n let failureReason: string | undefined;\n let timedOut = false;\n let settled = false;\n\n // Returns false when an earlier kill already recorded its reason, so callers can tell whether theirs won.\n const killProcessGroup = (reason: string): boolean => {\n if (failureReason !== undefined) return false;\n failureReason = reason;\n // Stop buffering immediately: OS pipe buffers can keep emitting data after the kill, and\n // destroyed streams also let `close` fire even if a stray grandchild inherited the pipes.\n child.stdout?.destroy();\n child.stderr?.destroy();\n try {\n if (child.pid === undefined) {\n child.kill('SIGKILL');\n } else {\n killHarnessTree(child.pid);\n }\n } catch {\n child.kill('SIGKILL');\n }\n return true;\n };\n const timeoutId = setTimeout(() => {\n timedOut = killProcessGroup(`timed out after ${options.timeoutMs / 1000} seconds`);\n }, options.timeoutMs);\n\n const appendOutput = (chunk: Buffer, chunks: Buffer[]): void => {\n totalOutputBytes += chunk.byteLength;\n if (totalOutputBytes > options.maxOutputBytes) {\n killProcessGroup(`the harness printed more than ${options.maxOutputBytes / 1024 / 1024} MB of output`);\n return;\n }\n chunks.push(chunk);\n };\n child.stdout?.on('data', (chunk: Buffer) => appendOutput(chunk, stdoutChunks));\n child.stderr?.on('data', (chunk: Buffer) => appendOutput(chunk, stderrChunks));\n\n const settle = (exitCode: number | undefined, signal: NodeJS.Signals | undefined, spawnError?: Error): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n if (liveRun) liveHarnessRuns.delete(liveRun);\n resolve({\n stdout: Buffer.concat(stdoutChunks).toString('utf8'),\n stderr: Buffer.concat(stderrChunks).toString('utf8'),\n exitCode,\n signal,\n failureReason: failureReason ?? (spawnError ? `failed to run the harness: ${spawnError.message}` : undefined),\n timedOut,\n });\n };\n child.on('error', (error) => settle(undefined, undefined, error));\n child.on('close', (exitCode, signal) => settle(exitCode ?? undefined, signal ?? undefined));\n });\n}\n\nfunction installSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, () => {\n for (const liveRun of liveHarnessRuns) {\n try {\n killHarnessTree(liveRun.pid);\n } catch {\n // The tree already exited.\n }\n // Best-effort cleanup while exiting; judged code may have left permission-locked entries.\n if (liveRun.tempRoot) forciblyRemoveDirectorySync(liveRun.tempRoot);\n }\n process.kill(process.pid, signal);\n });\n }\n}\n\n// On Windows the harness is not detached (process groups are unavailable), so killing only the\n// direct child would leave submission grandchildren running; taskkill terminates the whole tree.\nfunction killHarnessTree(pid: number): void {\n if (process.platform === 'win32') {\n child_process.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });\n } else {\n process.kill(-pid, 'SIGKILL');\n }\n}\n"],"mappings":"kKAiCA,MAAM,EAAkB,IAAI,IAC5B,IAAI,EAA0B,GAS9B,SAAgB,EACd,EACA,EAC+B,CAE/B,OADA,EAAsB,EACf,IAAI,QAAS,GAAY,CAE9B,IAAM,EAAQA,EAAAA,QAAc,MAAM,QAAQ,SAAU,EAAa,CAC/D,IAAK,EAAQ,IACb,SAAU,QAAQ,WAAa,QAE/B,IAAK,EAAQ,SAAW,CAAE,GAAG,EAAQ,IAAK,OAAQ,EAAQ,QAAS,EAAI,EAAQ,IAC/E,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EACK,EACJ,EAAM,MAAQ,IAAA,GAAY,IAAA,GAAY,CAAE,IAAK,EAAM,IAAK,SAAU,EAAQ,QAAS,EACjF,GAAS,EAAgB,IAAI,CAAO,EACxC,IAAM,EAAyB,CAAC,EAC1B,EAAyB,CAAC,EAC5B,EAAmB,EACnB,EACA,EAAW,GACX,EAAU,GAGR,EAAoB,GAA4B,CACpD,GAAI,IAAkB,IAAA,GAAW,MAAO,GACxC,EAAgB,EAGhB,EAAM,QAAQ,QAAQ,EACtB,EAAM,QAAQ,QAAQ,EACtB,GAAI,CACE,EAAM,MAAQ,IAAA,GAChB,EAAM,KAAK,SAAS,EAEpB,EAAgB,EAAM,GAAG,CAE7B,MAAQ,CACN,EAAM,KAAK,SAAS,CACtB,CACA,MAAO,EACT,EACM,EAAY,eAAiB,CACjC,EAAW,EAAiB,mBAAmB,EAAQ,UAAY,IAAK,SAAS,CACnF,EAAG,EAAQ,SAAS,EAEd,GAAgB,EAAe,IAA2B,CAE9D,GADA,GAAoB,EAAM,WACtB,EAAmB,EAAQ,eAAgB,CAC7C,EAAiB,iCAAiC,EAAQ,eAAiB,KAAO,KAAK,cAAc,EACrG,MACF,CACA,EAAO,KAAK,CAAK,CACnB,EACA,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAC7E,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAE7E,IAAM,GAAU,EAA8B,EAAoC,IAA6B,CACzG,IACJ,EAAU,GACV,aAAa,CAAS,EAClB,GAAS,EAAgB,OAAO,CAAO,EAC3C,EAAQ,CACN,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,WACA,SACA,cAAe,IAAkB,EAAa,8BAA8B,EAAW,UAAY,IAAA,IACnG,UACF,CAAC,EACH,EACA,EAAM,GAAG,QAAU,GAAU,EAAO,IAAA,GAAW,IAAA,GAAW,CAAK,CAAC,EAChE,EAAM,GAAG,SAAU,EAAU,IAAW,EAAO,GAAY,IAAA,GAAW,GAAU,IAAA,EAAS,CAAC,CAC5F,CAAC,CACH,CAEA,SAAS,GAA8B,CACjC,MACJ,GAA0B,GAC1B,IAAK,IAAM,IAAU,CAAC,SAAU,SAAS,EACvC,QAAQ,KAAK,MAAc,CACzB,IAAK,IAAM,KAAW,EAAiB,CACrC,GAAI,CACF,EAAgB,EAAQ,GAAG,CAC7B,MAAQ,CAER,CAEI,EAAQ,UAAU,EAAA,4BAA4B,EAAQ,QAAQ,CACpE,CACA,QAAQ,KAAK,QAAQ,IAAK,CAAM,CAClC,CAAC,CAbuB,CAe5B,CAIA,SAAS,EAAgB,EAAmB,CACtC,QAAQ,WAAa,QACvB,EAAA,QAAc,UAAU,WAAY,CAAC,OAAQ,OAAO,CAAG,EAAG,KAAM,IAAI,EAAG,CAAE,MAAO,QAAS,CAAC,EAE1F,QAAQ,KAAK,CAAC,EAAK,SAAS,CAEhC"}
@@ -1,2 +1,2 @@
1
- import{forciblyRemoveDirectorySync as e}from"./temporaryProblemDirCopy.js";import t from"node:child_process";const n=new Set;let r=!1;function i(e,r){return a(),new Promise(i=>{let a=t.spawn(process.execPath,e,{cwd:r.cwd,detached:process.platform!==`win32`,env:r.env,stdio:[`ignore`,`pipe`,`pipe`]}),s=a.pid===void 0?void 0:{pid:a.pid,tempRoot:r.tempRoot};s&&n.add(s);let c=[],l=[],u=0,d,f=!1,p=!1,m=e=>{if(d!==void 0)return!1;d=e,a.stdout?.destroy(),a.stderr?.destroy();try{a.pid===void 0?a.kill(`SIGKILL`):o(a.pid)}catch{a.kill(`SIGKILL`)}return!0},h=setTimeout(()=>{f=m(`timed out after ${r.timeoutMs/1e3} seconds`)},r.timeoutMs),g=(e,t)=>{if(u+=e.byteLength,u>r.maxOutputBytes){m(`the harness printed more than ${r.maxOutputBytes/1024/1024} MB of output`);return}t.push(e)};a.stdout?.on(`data`,e=>g(e,c)),a.stderr?.on(`data`,e=>g(e,l));let _=(e,t,r)=>{p||(p=!0,clearTimeout(h),s&&n.delete(s),i({stdout:Buffer.concat(c).toString(`utf8`),stderr:Buffer.concat(l).toString(`utf8`),exitCode:e,signal:t,failureReason:d??(r?`failed to run the harness: ${r.message}`:void 0),timedOut:f}))};a.on(`error`,e=>_(void 0,void 0,e)),a.on(`close`,(e,t)=>_(e??void 0,t??void 0))})}function a(){if(!r){r=!0;for(let t of[`SIGINT`,`SIGTERM`])process.once(t,()=>{for(let t of n){try{o(t.pid)}catch{}t.tempRoot&&e(t.tempRoot)}process.kill(process.pid,t)})}}function o(e){process.platform===`win32`?t.spawnSync(`taskkill`,[`/PID`,String(e),`/T`,`/F`],{stdio:`ignore`}):process.kill(-e,`SIGKILL`)}export{i as runHarnessProcess};
1
+ import{forciblyRemoveDirectorySync as e}from"./temporaryProblemDirCopy.js";import t from"node:child_process";const n=new Set;let r=!1;function i(e,r){return a(),new Promise(i=>{let a=t.spawn(process.execPath,e,{cwd:r.cwd,detached:process.platform!==`win32`,env:r.tempRoot?{...r.env,TMPDIR:r.tempRoot}:r.env,stdio:[`ignore`,`pipe`,`pipe`]}),s=a.pid===void 0?void 0:{pid:a.pid,tempRoot:r.tempRoot};s&&n.add(s);let c=[],l=[],u=0,d,f=!1,p=!1,m=e=>{if(d!==void 0)return!1;d=e,a.stdout?.destroy(),a.stderr?.destroy();try{a.pid===void 0?a.kill(`SIGKILL`):o(a.pid)}catch{a.kill(`SIGKILL`)}return!0},h=setTimeout(()=>{f=m(`timed out after ${r.timeoutMs/1e3} seconds`)},r.timeoutMs),g=(e,t)=>{if(u+=e.byteLength,u>r.maxOutputBytes){m(`the harness printed more than ${r.maxOutputBytes/1024/1024} MB of output`);return}t.push(e)};a.stdout?.on(`data`,e=>g(e,c)),a.stderr?.on(`data`,e=>g(e,l));let _=(e,t,r)=>{p||(p=!0,clearTimeout(h),s&&n.delete(s),i({stdout:Buffer.concat(c).toString(`utf8`),stderr:Buffer.concat(l).toString(`utf8`),exitCode:e,signal:t,failureReason:d??(r?`failed to run the harness: ${r.message}`:void 0),timedOut:f}))};a.on(`error`,e=>_(void 0,void 0,e)),a.on(`close`,(e,t)=>_(e??void 0,t??void 0))})}function a(){if(!r){r=!0;for(let t of[`SIGINT`,`SIGTERM`])process.once(t,()=>{for(let t of n){try{o(t.pid)}catch{}t.tempRoot&&e(t.tempRoot)}process.kill(process.pid,t)})}}function o(e){process.platform===`win32`?t.spawnSync(`taskkill`,[`/PID`,String(e),`/T`,`/F`],{stdio:`ignore`}):process.kill(-e,`SIGKILL`)}export{i as runHarnessProcess};
2
2
  //# sourceMappingURL=runHarnessProcess.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runHarnessProcess.js","names":["child_process"],"sources":["../../src/helpers/runHarnessProcess.ts"],"sourcesContent":["import child_process from 'node:child_process';\n\nimport { forciblyRemoveDirectorySync } from './temporaryProblemDirCopy.js';\n\nexport interface HarnessProcessOptions {\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeoutMs: number;\n maxOutputBytes: number;\n /** A temporary copy to remove when the check itself is interrupted by a signal. */\n tempRoot?: string;\n}\n\nexport interface HarnessProcessResult {\n stdout: string;\n stderr: string;\n exitCode: number | undefined;\n /** The signal that terminated the harness when it did not exit on its own. */\n signal: NodeJS.Signals | undefined;\n /** Set when the run was killed by this helper (timeout or output cap) or could not be spawned. */\n failureReason: string | undefined;\n timedOut: boolean;\n}\n\ninterface LiveHarnessRun {\n pid: number;\n tempRoot: string | undefined;\n}\n\n// Detached harness groups no longer receive the terminal's SIGINT, so an interrupted run must\n// tear them down (and remove their temp copies) itself before exiting. The handlers stay installed\n// for the process lifetime: once the last run has settled they only iterate an empty set and re-raise\n// the signal, so removing them would change nothing observable.\nconst liveHarnessRuns = new Set<LiveHarnessRun>();\nlet signalHandlersInstalled = false;\n\n/**\n * Run a judge harness with `process.execPath` in its own process group, killing the whole group on\n * timeout or when the output cap is exceeded. The submissions a harness judges through\n * `spawnWithLimits` run in sessions of their own, out of that kill's reach; each wrapper's process\n * group is bounded by its own GNU `timeout` instead, while a descendant that moved to yet another\n * session may survive without stalling the harness.\n */\nexport function runHarnessProcess(\n commandArgs: readonly string[],\n options: HarnessProcessOptions\n): Promise<HarnessProcessResult> {\n installSignalHandlers();\n return new Promise((resolve) => {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const child = child_process.spawn(process.execPath, commandArgs, {\n cwd: options.cwd,\n detached: process.platform !== 'win32',\n env: options.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const liveRun: LiveHarnessRun | undefined =\n child.pid === undefined ? undefined : { pid: child.pid, tempRoot: options.tempRoot };\n if (liveRun) liveHarnessRuns.add(liveRun);\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let totalOutputBytes = 0;\n let failureReason: string | undefined;\n let timedOut = false;\n let settled = false;\n\n // Returns false when an earlier kill already recorded its reason, so callers can tell whether theirs won.\n const killProcessGroup = (reason: string): boolean => {\n if (failureReason !== undefined) return false;\n failureReason = reason;\n // Stop buffering immediately: OS pipe buffers can keep emitting data after the kill, and\n // destroyed streams also let `close` fire even if a stray grandchild inherited the pipes.\n child.stdout?.destroy();\n child.stderr?.destroy();\n try {\n if (child.pid === undefined) {\n child.kill('SIGKILL');\n } else {\n killHarnessTree(child.pid);\n }\n } catch {\n child.kill('SIGKILL');\n }\n return true;\n };\n const timeoutId = setTimeout(() => {\n timedOut = killProcessGroup(`timed out after ${options.timeoutMs / 1000} seconds`);\n }, options.timeoutMs);\n\n const appendOutput = (chunk: Buffer, chunks: Buffer[]): void => {\n totalOutputBytes += chunk.byteLength;\n if (totalOutputBytes > options.maxOutputBytes) {\n killProcessGroup(`the harness printed more than ${options.maxOutputBytes / 1024 / 1024} MB of output`);\n return;\n }\n chunks.push(chunk);\n };\n child.stdout?.on('data', (chunk: Buffer) => appendOutput(chunk, stdoutChunks));\n child.stderr?.on('data', (chunk: Buffer) => appendOutput(chunk, stderrChunks));\n\n const settle = (exitCode: number | undefined, signal: NodeJS.Signals | undefined, spawnError?: Error): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n if (liveRun) liveHarnessRuns.delete(liveRun);\n resolve({\n stdout: Buffer.concat(stdoutChunks).toString('utf8'),\n stderr: Buffer.concat(stderrChunks).toString('utf8'),\n exitCode,\n signal,\n failureReason: failureReason ?? (spawnError ? `failed to run the harness: ${spawnError.message}` : undefined),\n timedOut,\n });\n };\n child.on('error', (error) => settle(undefined, undefined, error));\n child.on('close', (exitCode, signal) => settle(exitCode ?? undefined, signal ?? undefined));\n });\n}\n\nfunction installSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, () => {\n for (const liveRun of liveHarnessRuns) {\n try {\n killHarnessTree(liveRun.pid);\n } catch {\n // The tree already exited.\n }\n // Best-effort cleanup while exiting; judged code may have left permission-locked entries.\n if (liveRun.tempRoot) forciblyRemoveDirectorySync(liveRun.tempRoot);\n }\n process.kill(process.pid, signal);\n });\n }\n}\n\n// On Windows the harness is not detached (process groups are unavailable), so killing only the\n// direct child would leave submission grandchildren running; taskkill terminates the whole tree.\nfunction killHarnessTree(pid: number): void {\n if (process.platform === 'win32') {\n child_process.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });\n } else {\n process.kill(-pid, 'SIGKILL');\n }\n}\n"],"mappings":"6GAiCA,MAAM,EAAkB,IAAI,IAC5B,IAAI,EAA0B,GAS9B,SAAgB,EACd,EACA,EAC+B,CAE/B,OADA,EAAsB,EACf,IAAI,QAAS,GAAY,CAE9B,IAAM,EAAQA,EAAc,MAAM,QAAQ,SAAU,EAAa,CAC/D,IAAK,EAAQ,IACb,SAAU,QAAQ,WAAa,QAC/B,IAAK,EAAQ,IACb,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EACK,EACJ,EAAM,MAAQ,IAAA,GAAY,IAAA,GAAY,CAAE,IAAK,EAAM,IAAK,SAAU,EAAQ,QAAS,EACjF,GAAS,EAAgB,IAAI,CAAO,EACxC,IAAM,EAAyB,CAAC,EAC1B,EAAyB,CAAC,EAC5B,EAAmB,EACnB,EACA,EAAW,GACX,EAAU,GAGR,EAAoB,GAA4B,CACpD,GAAI,IAAkB,IAAA,GAAW,MAAO,GACxC,EAAgB,EAGhB,EAAM,QAAQ,QAAQ,EACtB,EAAM,QAAQ,QAAQ,EACtB,GAAI,CACE,EAAM,MAAQ,IAAA,GAChB,EAAM,KAAK,SAAS,EAEpB,EAAgB,EAAM,GAAG,CAE7B,MAAQ,CACN,EAAM,KAAK,SAAS,CACtB,CACA,MAAO,EACT,EACM,EAAY,eAAiB,CACjC,EAAW,EAAiB,mBAAmB,EAAQ,UAAY,IAAK,SAAS,CACnF,EAAG,EAAQ,SAAS,EAEd,GAAgB,EAAe,IAA2B,CAE9D,GADA,GAAoB,EAAM,WACtB,EAAmB,EAAQ,eAAgB,CAC7C,EAAiB,iCAAiC,EAAQ,eAAiB,KAAO,KAAK,cAAc,EACrG,MACF,CACA,EAAO,KAAK,CAAK,CACnB,EACA,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAC7E,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAE7E,IAAM,GAAU,EAA8B,EAAoC,IAA6B,CACzG,IACJ,EAAU,GACV,aAAa,CAAS,EAClB,GAAS,EAAgB,OAAO,CAAO,EAC3C,EAAQ,CACN,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,WACA,SACA,cAAe,IAAkB,EAAa,8BAA8B,EAAW,UAAY,IAAA,IACnG,UACF,CAAC,EACH,EACA,EAAM,GAAG,QAAU,GAAU,EAAO,IAAA,GAAW,IAAA,GAAW,CAAK,CAAC,EAChE,EAAM,GAAG,SAAU,EAAU,IAAW,EAAO,GAAY,IAAA,GAAW,GAAU,IAAA,EAAS,CAAC,CAC5F,CAAC,CACH,CAEA,SAAS,GAA8B,CACjC,MACJ,GAA0B,GAC1B,IAAK,IAAM,IAAU,CAAC,SAAU,SAAS,EACvC,QAAQ,KAAK,MAAc,CACzB,IAAK,IAAM,KAAW,EAAiB,CACrC,GAAI,CACF,EAAgB,EAAQ,GAAG,CAC7B,MAAQ,CAER,CAEI,EAAQ,UAAU,EAA4B,EAAQ,QAAQ,CACpE,CACA,QAAQ,KAAK,QAAQ,IAAK,CAAM,CAClC,CAAC,CAbuB,CAe5B,CAIA,SAAS,EAAgB,EAAmB,CACtC,QAAQ,WAAa,QACvB,EAAc,UAAU,WAAY,CAAC,OAAQ,OAAO,CAAG,EAAG,KAAM,IAAI,EAAG,CAAE,MAAO,QAAS,CAAC,EAE1F,QAAQ,KAAK,CAAC,EAAK,SAAS,CAEhC"}
1
+ {"version":3,"file":"runHarnessProcess.js","names":["child_process"],"sources":["../../src/helpers/runHarnessProcess.ts"],"sourcesContent":["import child_process from 'node:child_process';\n\nimport { forciblyRemoveDirectorySync } from './temporaryProblemDirCopy.js';\n\nexport interface HarnessProcessOptions {\n cwd: string;\n env: NodeJS.ProcessEnv;\n timeoutMs: number;\n maxOutputBytes: number;\n /** A temporary copy to remove when the check itself is interrupted by a signal. */\n tempRoot?: string;\n}\n\nexport interface HarnessProcessResult {\n stdout: string;\n stderr: string;\n exitCode: number | undefined;\n /** The signal that terminated the harness when it did not exit on its own. */\n signal: NodeJS.Signals | undefined;\n /** Set when the run was killed by this helper (timeout or output cap) or could not be spawned. */\n failureReason: string | undefined;\n timedOut: boolean;\n}\n\ninterface LiveHarnessRun {\n pid: number;\n tempRoot: string | undefined;\n}\n\n// Detached harness groups no longer receive the terminal's SIGINT, so an interrupted run must\n// tear them down (and remove their temp copies) itself before exiting. The handlers stay installed\n// for the process lifetime: once the last run has settled they only iterate an empty set and re-raise\n// the signal, so removing them would change nothing observable.\nconst liveHarnessRuns = new Set<LiveHarnessRun>();\nlet signalHandlersInstalled = false;\n\n/**\n * Run a judge harness with `process.execPath` in its own process group, killing the whole group on\n * timeout or when the output cap is exceeded. The submissions a harness judges through\n * `spawnWithLimits` run in sessions of their own, out of that kill's reach; each wrapper's process\n * group is bounded by its own GNU `timeout` instead, while a descendant that moved to yet another\n * session may survive without stalling the harness.\n */\nexport function runHarnessProcess(\n commandArgs: readonly string[],\n options: HarnessProcessOptions\n): Promise<HarnessProcessResult> {\n installSignalHandlers();\n return new Promise((resolve) => {\n // process.execPath keeps the harness on the same bun executable regardless of PATH.\n const child = child_process.spawn(process.execPath, commandArgs, {\n cwd: options.cwd,\n detached: process.platform !== 'win32',\n // The parent removes this root even when SIGKILL prevents child cleanup.\n env: options.tempRoot ? { ...options.env, TMPDIR: options.tempRoot } : options.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const liveRun: LiveHarnessRun | undefined =\n child.pid === undefined ? undefined : { pid: child.pid, tempRoot: options.tempRoot };\n if (liveRun) liveHarnessRuns.add(liveRun);\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let totalOutputBytes = 0;\n let failureReason: string | undefined;\n let timedOut = false;\n let settled = false;\n\n // Returns false when an earlier kill already recorded its reason, so callers can tell whether theirs won.\n const killProcessGroup = (reason: string): boolean => {\n if (failureReason !== undefined) return false;\n failureReason = reason;\n // Stop buffering immediately: OS pipe buffers can keep emitting data after the kill, and\n // destroyed streams also let `close` fire even if a stray grandchild inherited the pipes.\n child.stdout?.destroy();\n child.stderr?.destroy();\n try {\n if (child.pid === undefined) {\n child.kill('SIGKILL');\n } else {\n killHarnessTree(child.pid);\n }\n } catch {\n child.kill('SIGKILL');\n }\n return true;\n };\n const timeoutId = setTimeout(() => {\n timedOut = killProcessGroup(`timed out after ${options.timeoutMs / 1000} seconds`);\n }, options.timeoutMs);\n\n const appendOutput = (chunk: Buffer, chunks: Buffer[]): void => {\n totalOutputBytes += chunk.byteLength;\n if (totalOutputBytes > options.maxOutputBytes) {\n killProcessGroup(`the harness printed more than ${options.maxOutputBytes / 1024 / 1024} MB of output`);\n return;\n }\n chunks.push(chunk);\n };\n child.stdout?.on('data', (chunk: Buffer) => appendOutput(chunk, stdoutChunks));\n child.stderr?.on('data', (chunk: Buffer) => appendOutput(chunk, stderrChunks));\n\n const settle = (exitCode: number | undefined, signal: NodeJS.Signals | undefined, spawnError?: Error): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutId);\n if (liveRun) liveHarnessRuns.delete(liveRun);\n resolve({\n stdout: Buffer.concat(stdoutChunks).toString('utf8'),\n stderr: Buffer.concat(stderrChunks).toString('utf8'),\n exitCode,\n signal,\n failureReason: failureReason ?? (spawnError ? `failed to run the harness: ${spawnError.message}` : undefined),\n timedOut,\n });\n };\n child.on('error', (error) => settle(undefined, undefined, error));\n child.on('close', (exitCode, signal) => settle(exitCode ?? undefined, signal ?? undefined));\n });\n}\n\nfunction installSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, () => {\n for (const liveRun of liveHarnessRuns) {\n try {\n killHarnessTree(liveRun.pid);\n } catch {\n // The tree already exited.\n }\n // Best-effort cleanup while exiting; judged code may have left permission-locked entries.\n if (liveRun.tempRoot) forciblyRemoveDirectorySync(liveRun.tempRoot);\n }\n process.kill(process.pid, signal);\n });\n }\n}\n\n// On Windows the harness is not detached (process groups are unavailable), so killing only the\n// direct child would leave submission grandchildren running; taskkill terminates the whole tree.\nfunction killHarnessTree(pid: number): void {\n if (process.platform === 'win32') {\n child_process.spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });\n } else {\n process.kill(-pid, 'SIGKILL');\n }\n}\n"],"mappings":"6GAiCA,MAAM,EAAkB,IAAI,IAC5B,IAAI,EAA0B,GAS9B,SAAgB,EACd,EACA,EAC+B,CAE/B,OADA,EAAsB,EACf,IAAI,QAAS,GAAY,CAE9B,IAAM,EAAQA,EAAc,MAAM,QAAQ,SAAU,EAAa,CAC/D,IAAK,EAAQ,IACb,SAAU,QAAQ,WAAa,QAE/B,IAAK,EAAQ,SAAW,CAAE,GAAG,EAAQ,IAAK,OAAQ,EAAQ,QAAS,EAAI,EAAQ,IAC/E,MAAO,CAAC,SAAU,OAAQ,MAAM,CAClC,CAAC,EACK,EACJ,EAAM,MAAQ,IAAA,GAAY,IAAA,GAAY,CAAE,IAAK,EAAM,IAAK,SAAU,EAAQ,QAAS,EACjF,GAAS,EAAgB,IAAI,CAAO,EACxC,IAAM,EAAyB,CAAC,EAC1B,EAAyB,CAAC,EAC5B,EAAmB,EACnB,EACA,EAAW,GACX,EAAU,GAGR,EAAoB,GAA4B,CACpD,GAAI,IAAkB,IAAA,GAAW,MAAO,GACxC,EAAgB,EAGhB,EAAM,QAAQ,QAAQ,EACtB,EAAM,QAAQ,QAAQ,EACtB,GAAI,CACE,EAAM,MAAQ,IAAA,GAChB,EAAM,KAAK,SAAS,EAEpB,EAAgB,EAAM,GAAG,CAE7B,MAAQ,CACN,EAAM,KAAK,SAAS,CACtB,CACA,MAAO,EACT,EACM,EAAY,eAAiB,CACjC,EAAW,EAAiB,mBAAmB,EAAQ,UAAY,IAAK,SAAS,CACnF,EAAG,EAAQ,SAAS,EAEd,GAAgB,EAAe,IAA2B,CAE9D,GADA,GAAoB,EAAM,WACtB,EAAmB,EAAQ,eAAgB,CAC7C,EAAiB,iCAAiC,EAAQ,eAAiB,KAAO,KAAK,cAAc,EACrG,MACF,CACA,EAAO,KAAK,CAAK,CACnB,EACA,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAC7E,EAAM,QAAQ,GAAG,OAAS,GAAkB,EAAa,EAAO,CAAY,CAAC,EAE7E,IAAM,GAAU,EAA8B,EAAoC,IAA6B,CACzG,IACJ,EAAU,GACV,aAAa,CAAS,EAClB,GAAS,EAAgB,OAAO,CAAO,EAC3C,EAAQ,CACN,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,OAAQ,OAAO,OAAO,CAAY,CAAC,CAAC,SAAS,MAAM,EACnD,WACA,SACA,cAAe,IAAkB,EAAa,8BAA8B,EAAW,UAAY,IAAA,IACnG,UACF,CAAC,EACH,EACA,EAAM,GAAG,QAAU,GAAU,EAAO,IAAA,GAAW,IAAA,GAAW,CAAK,CAAC,EAChE,EAAM,GAAG,SAAU,EAAU,IAAW,EAAO,GAAY,IAAA,GAAW,GAAU,IAAA,EAAS,CAAC,CAC5F,CAAC,CACH,CAEA,SAAS,GAA8B,CACjC,MACJ,GAA0B,GAC1B,IAAK,IAAM,IAAU,CAAC,SAAU,SAAS,EACvC,QAAQ,KAAK,MAAc,CACzB,IAAK,IAAM,KAAW,EAAiB,CACrC,GAAI,CACF,EAAgB,EAAQ,GAAG,CAC7B,MAAQ,CAER,CAEI,EAAQ,UAAU,EAA4B,EAAQ,QAAQ,CACpE,CACA,QAAQ,KAAK,QAAQ,IAAK,CAAM,CAClC,CAAC,CAbuB,CAe5B,CAIA,SAAS,EAAgB,EAAmB,CACtC,QAAQ,WAAa,QACvB,EAAc,UAAU,WAAY,CAAC,OAAQ,OAAO,CAAG,EAAG,KAAM,IAAI,EAAG,CAAE,MAAO,QAAS,CAAC,EAE1F,QAAQ,KAAK,CAAC,EAAK,SAAS,CAEhC"}
@@ -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("node:http");r=e.__toESM(r,1);function i(e){let i=r.default.createServer((r,i)=>{let a=new URL(r.url??`/`,`http://127.0.0.1`).pathname;a===`/`&&(a=`/index.html`);let s=a.endsWith(`/`)?n.default.join(a,`index.html`):a,c=n.default.join(e,s);if(t.default.existsSync(c))try{let e=t.default.readFileSync(c);i.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":o(s)}),i.end(e)}catch{i.statusCode=500,i.end()}else i.statusCode=404,i.end()});i.listen();let a=i.address();if(!a)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{i.closeAllConnections(),await new Promise(e=>{i.close(()=>{e()})})},url:typeof a==`string`?a:`http://127.0.0.1:${a.port}`,port:typeof a==`object`?a.port:void 0}}const a={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function o(e){for(let[t,n]of Object.entries(a))if(e.endsWith(t))return n;return`text/plain`}exports.startHttpServer=i;
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("node:http");r=e.__toESM(r,1);function i(e){let i=r.default.createServer((r,i)=>{let a;try{let e=new URL(r.url??`/`,`http://127.0.0.1`).pathname;a=decodeURIComponent(e.replaceAll(/%(?![\da-f]{2})/giu,`%25`))}catch{i.writeHead(400),i.end();return}a===`/`&&(a=`/index.html`);let s=a.endsWith(`/`)?n.default.join(a,`index.html`):a,c=n.default.join(e,s);if(t.default.existsSync(c))try{let e=t.default.readFileSync(c);i.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":o(s)}),i.end(e)}catch{i.statusCode=500,i.end()}else i.statusCode=404,i.end()});i.listen();let a=i.address();if(!a)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{i.closeAllConnections(),await new Promise(e=>{i.close(()=>{e()})})},url:typeof a==`string`?a:`http://127.0.0.1:${a.port}`,port:typeof a==`object`?a.port:void 0}}const a={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function o(e){for(let[t,n]of Object.entries(a))if(e.endsWith(t))return n;return`text/plain`}exports.startHttpServer=i;
2
2
  //# sourceMappingURL=startHttpServer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"startHttpServer.cjs","names":["http","path","fs"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"oMAaA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAASA,EAAAA,QAAK,cAAc,EAAS,IAAa,CACtD,IAAI,EAAW,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SAE3D,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAIC,EAAAA,QAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAWA,EAAAA,QAAK,KAAK,EAAK,CAAqB,EAErD,GAAIC,EAAAA,QAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAASA,EAAAA,QAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,EAED,EAAO,OAAO,EAEd,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
1
+ {"version":3,"file":"startHttpServer.cjs","names":["http","path","fs"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"oMAaA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAASA,EAAAA,QAAK,cAAc,EAAS,IAAa,CACtD,IAAI,EACJ,GAAI,CACF,IAAM,EAAkB,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmB,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACN,EAAS,UAAU,GAAG,EACtB,EAAS,IAAI,EACb,MACF,CAEI,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAIC,EAAAA,QAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAWA,EAAAA,QAAK,KAAK,EAAK,CAAqB,EAErD,GAAIC,EAAAA,QAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAASA,EAAAA,QAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,EAED,EAAO,OAAO,EAEd,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
@@ -1,2 +1,2 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:http";function r(r){let i=n.createServer((n,i)=>{let o=new URL(n.url??`/`,`http://127.0.0.1`).pathname;o===`/`&&(o=`/index.html`);let s=o.endsWith(`/`)?t.join(o,`index.html`):o,c=t.join(r,s);if(e.existsSync(c))try{let t=e.readFileSync(c);i.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":a(s)}),i.end(t)}catch{i.statusCode=500,i.end()}else i.statusCode=404,i.end()});i.listen();let o=i.address();if(!o)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{i.closeAllConnections(),await new Promise(e=>{i.close(()=>{e()})})},url:typeof o==`string`?o:`http://127.0.0.1:${o.port}`,port:typeof o==`object`?o.port:void 0}}const i={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function a(e){for(let[t,n]of Object.entries(i))if(e.endsWith(t))return n;return`text/plain`}export{r as startHttpServer};
1
+ import e from"node:fs";import t from"node:path";import n from"node:http";function r(r){let i=n.createServer((n,i)=>{let o;try{let e=new URL(n.url??`/`,`http://127.0.0.1`).pathname;o=decodeURIComponent(e.replaceAll(/%(?![\da-f]{2})/giu,`%25`))}catch{i.writeHead(400),i.end();return}o===`/`&&(o=`/index.html`);let s=o.endsWith(`/`)?t.join(o,`index.html`):o,c=t.join(r,s);if(e.existsSync(c))try{let t=e.readFileSync(c);i.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":a(s)}),i.end(t)}catch{i.statusCode=500,i.end()}else i.statusCode=404,i.end()});i.listen();let o=i.address();if(!o)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{i.closeAllConnections(),await new Promise(e=>{i.close(()=>{e()})})},url:typeof o==`string`?o:`http://127.0.0.1:${o.port}`,port:typeof o==`object`?o.port:void 0}}const i={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function a(e){for(let[t,n]of Object.entries(i))if(e.endsWith(t))return n;return`text/plain`}export{r as startHttpServer};
2
2
  //# sourceMappingURL=startHttpServer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"startHttpServer.js","names":[],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"yEAaA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAK,cAAc,EAAS,IAAa,CACtD,IAAI,EAAW,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SAE3D,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAI,EAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAW,EAAK,KAAK,EAAK,CAAqB,EAErD,GAAI,EAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAAS,EAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,EAED,EAAO,OAAO,EAEd,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
1
+ {"version":3,"file":"startHttpServer.js","names":[],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/**\n * Start a HTTP server for testing web pages.\n */\nexport function startHttpServer(dir: string): HttpServer {\n const server = http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.join(dir, pathnameWithIndexHtml);\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n\n server.listen();\n\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"yEAaA,SAAgB,EAAgB,EAAyB,CACvD,IAAM,EAAS,EAAK,cAAc,EAAS,IAAa,CACtD,IAAI,EACJ,GAAI,CACF,IAAM,EAAkB,IAAI,IAAI,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmB,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACN,EAAS,UAAU,GAAG,EACtB,EAAS,IAAI,EACb,MACF,CAEI,IAAa,MACf,EAAW,eAGb,IAAM,EAAwB,EAAS,SAAS,GAAG,EAAI,EAAK,KAAK,EAAU,YAAY,EAAI,EAErF,EAAW,EAAK,KAAK,EAAK,CAAqB,EAErD,GAAI,EAAG,WAAW,CAAQ,EACxB,GAAI,CACF,IAAM,EAAS,EAAG,aAAa,CAAQ,EACvC,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgB,EAAe,CAAqB,CACtD,CAAC,EACD,EAAS,IAAI,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtB,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtB,EAAS,IAAI,CAEjB,CAAC,EAED,EAAO,OAAO,EAEd,IAAM,EAAU,EAAO,QAAQ,EAC/B,GAAI,CAAC,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjC,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnC,EAAO,UAAY,CACjB,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAO,GAAY,SAAW,EAAU,oBAAoB,EAAQ,OACzE,KAAM,OAAO,GAAY,SAAW,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAM,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAAS,EAAe,EAA0B,CAChD,IAAK,GAAM,CAAC,EAAQ,KAAgB,OAAO,QAAQ,CAAsB,EACvE,GAAI,EAAS,SAAS,CAAM,EAAG,OAAO,EAExC,MAAO,YACT"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/compareStdoutAsSpaceSeparatedTokens.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/compareExpectedOutputFiles.cjs"),i=require("./helpers/defaultStdioHarness.cjs"),a=require("./helpers/parseArgs.cjs"),o=require("./helpers/parseCsv.cjs"),s=require("./helpers/readTestCases.cjs"),c=require("./helpers/removeCommentsInSourceCode.cjs"),l=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),u=require("./helpers/runPythonInTemporaryUvProject.cjs"),d=require("./helpers/sourceCodeGrammars.cjs"),f=require("./helpers/startHttpServer.cjs"),p=require("./types/problem.cjs"),m=require("./learningMaterial/validationResult.cjs"),h=require("./learningMaterial/validateMaterial.cjs"),g=require("./learningMaterial/validateContest.cjs"),_=require("./learningMaterial/validateCourse.cjs"),v=require("./learningMaterial/validateProblem.cjs"),y=require("./types/decisionCode.cjs");exports.CsvParseError=o.CsvParseError,exports.DecisionCode=y.DecisionCode,exports.HARNESS_FILE_PRESETS=i.HARNESS_FILE_PRESETS,exports.MAX_COMPARED_FILE_BYTES=r.MAX_COMPARED_FILE_BYTES,exports.MAX_REPORTED_FILE_BYTES=r.MAX_REPORTED_FILE_BYTES,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.codeRuleSchema=p.codeRuleSchema,exports.compareExpectedOutputFiles=r.compareExpectedOutputFiles,exports.compareStdoutAsSpaceSeparatedTokens=e.compareStdoutAsSpaceSeparatedTokens,exports.copyPackageManagerProjectFiles=l.copyPackageManagerProjectFiles,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=i.findDefaultStdioHarnessFiles,exports.formatValidationReport=m.formatValidationReport,exports.formatZodIssues=m.formatZodIssues,exports.isDefaultStdioHarnessSource=i.isDefaultStdioHarnessSource,exports.judgeAgainstExpectations=r.judgeAgainstExpectations,exports.languageIdToSourceCodeGrammar=d.languageIdToSourceCodeGrammar,exports.mergeComparisonOutputFiles=r.mergeComparisonOutputFiles,exports.mergeSubmissionPeriods=h.mergeSubmissionPeriods,exports.normalizeCodeRule=p.normalizeCodeRule,exports.parseArgs=a.parseArgs,exports.parseCsv=o.parseCsv,exports.parseCsvRecords=o.parseCsvRecords,exports.printTestCaseResult=n.printTestCaseResult,exports.problemMarkdownFrontMatterSchema=p.problemMarkdownFrontMatterSchema,exports.readTestCases=s.readTestCases,exports.removeCommentsAndStringsInSourceCode=c.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=c.removeCommentsInSourceCode,exports.reportDuplicateIds=m.reportDuplicateIds,exports.reportExcessChatMarkers=h.reportExcessChatMarkers,exports.reportForeignProblemCourseIds=h.reportForeignProblemCourseIds,exports.runCommandInTemporaryPackageManagerProject=l.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=u.runPythonInTemporaryUvProject,exports.startHttpServer=f.startHttpServer,exports.testCaseResultSchema=t.testCaseResultSchema,exports.validateContestFile=g.validateContestFile,exports.validateCourseDirectory=_.validateCourseDirectory,exports.validateMaterialFile=h.validateMaterialFile,exports.validateProblemDirectory=v.validateProblemDirectory,exports.validateSubmissionPeriods=h.validateSubmissionPeriods;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/compareStdoutAsSpaceSeparatedTokens.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/compareExpectedOutputFiles.cjs"),i=require("./helpers/defaultStdioHarness.cjs"),a=require("./helpers/parseArgs.cjs"),o=require("./helpers/parseCsv.cjs"),s=require("./helpers/readTestCases.cjs"),c=require("./helpers/removeCommentsInSourceCode.cjs"),l=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),u=require("./helpers/runPythonInTemporaryUvProject.cjs"),d=require("./helpers/sourceCodeGrammars.cjs"),f=require("./helpers/startHttpServer.cjs"),p=require("./types/problem.cjs"),m=require("./learningMaterial/frontmatter.cjs"),h=require("./learningMaterial/validationResult.cjs"),g=require("./learningMaterial/validateMaterial.cjs"),_=require("./learningMaterial/validateContest.cjs"),v=require("./learningMaterial/validateCourse.cjs"),y=require("./learningMaterial/validateProblem.cjs"),b=require("./types/decisionCode.cjs");exports.CsvParseError=o.CsvParseError,exports.DecisionCode=b.DecisionCode,exports.HARNESS_FILE_PRESETS=i.HARNESS_FILE_PRESETS,exports.MAX_COMPARED_FILE_BYTES=r.MAX_COMPARED_FILE_BYTES,exports.MAX_REPORTED_FILE_BYTES=r.MAX_REPORTED_FILE_BYTES,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.codeRuleSchema=p.codeRuleSchema,exports.compareExpectedOutputFiles=r.compareExpectedOutputFiles,exports.compareStdoutAsSpaceSeparatedTokens=e.compareStdoutAsSpaceSeparatedTokens,exports.copyPackageManagerProjectFiles=l.copyPackageManagerProjectFiles,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=i.findDefaultStdioHarnessFiles,exports.formatValidationReport=h.formatValidationReport,exports.formatZodIssues=h.formatZodIssues,exports.isDefaultStdioHarnessSource=i.isDefaultStdioHarnessSource,exports.judgeAgainstExpectations=r.judgeAgainstExpectations,exports.languageIdToSourceCodeGrammar=d.languageIdToSourceCodeGrammar,exports.mergeComparisonOutputFiles=r.mergeComparisonOutputFiles,exports.mergeSubmissionPeriods=g.mergeSubmissionPeriods,exports.normalizeCodeRule=p.normalizeCodeRule,exports.parseArgs=a.parseArgs,exports.parseCsv=o.parseCsv,exports.parseCsvRecords=o.parseCsvRecords,exports.parseFrontmatter=m.parseFrontmatter,exports.printTestCaseResult=n.printTestCaseResult,exports.problemMarkdownFrontMatterSchema=p.problemMarkdownFrontMatterSchema,exports.readTestCases=s.readTestCases,exports.removeCommentsAndStringsInSourceCode=c.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=c.removeCommentsInSourceCode,exports.reportDuplicateIds=h.reportDuplicateIds,exports.reportExcessChatMarkers=g.reportExcessChatMarkers,exports.reportForeignProblemCourseIds=g.reportForeignProblemCourseIds,exports.runCommandInTemporaryPackageManagerProject=l.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=u.runPythonInTemporaryUvProject,exports.startHttpServer=f.startHttpServer,exports.testCaseResultSchema=t.testCaseResultSchema,exports.validateContestFile=_.validateContestFile,exports.validateCourseDirectory=v.validateCourseDirectory,exports.validateMaterialFile=g.validateMaterialFile,exports.validateProblemDirectory=y.validateProblemDirectory,exports.validateSubmissionPeriods=g.validateSubmissionPeriods;
package/dist/index.d.ts CHANGED
@@ -18,3 +18,4 @@ export * from './learningMaterial/validationResult.js';
18
18
  export * from './types/decisionCode.js';
19
19
  export * from './types/problem.js';
20
20
  export * from './types/testCaseResult.js';
21
+ export { parseFrontmatter } from './learningMaterial/frontmatter.js';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{compareStdoutAsSpaceSeparatedTokens as e}from"./helpers/compareStdoutAsSpaceSeparatedTokens.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{MAX_COMPARED_FILE_BYTES as a,MAX_REPORTED_FILE_BYTES as o,compareExpectedOutputFiles as s,judgeAgainstExpectations as c,mergeComparisonOutputFiles as l}from"./helpers/compareExpectedOutputFiles.js";import{HARNESS_FILE_PRESETS as u,findDefaultStdioHarnessFiles as d,isDefaultStdioHarnessSource as f}from"./helpers/defaultStdioHarness.js";import{parseArgs as p}from"./helpers/parseArgs.js";import{CsvParseError as m,parseCsv as h,parseCsvRecords as g}from"./helpers/parseCsv.js";import{readTestCases as _}from"./helpers/readTestCases.js";import{removeCommentsAndStringsInSourceCode as v,removeCommentsInSourceCode as y}from"./helpers/removeCommentsInSourceCode.js";import{copyPackageManagerProjectFiles as b,runCommandInTemporaryPackageManagerProject as x}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as S}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as C}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as w}from"./helpers/startHttpServer.js";import{codeRuleSchema as T,normalizeCodeRule as E,problemMarkdownFrontMatterSchema as D}from"./types/problem.js";import{formatValidationReport as O,formatZodIssues as k,reportDuplicateIds as A}from"./learningMaterial/validationResult.js";import{mergeSubmissionPeriods as j,reportExcessChatMarkers as M,reportForeignProblemCourseIds as N,validateMaterialFile as P,validateSubmissionPeriods as F}from"./learningMaterial/validateMaterial.js";import{validateContestFile as I}from"./learningMaterial/validateContest.js";import{validateCourseDirectory as L}from"./learningMaterial/validateCourse.js";import{validateProblemDirectory as R}from"./learningMaterial/validateProblem.js";import{DecisionCode as z}from"./types/decisionCode.js";export{m as CsvParseError,z as DecisionCode,u as HARNESS_FILE_PRESETS,a as MAX_COMPARED_FILE_BYTES,o as MAX_REPORTED_FILE_BYTES,t as TEST_CASE_RESULT_PREFIX,T as codeRuleSchema,s as compareExpectedOutputFiles,e as compareStdoutAsSpaceSeparatedTokens,b as copyPackageManagerProjectFiles,r as encodeFileForTestCaseResult,d as findDefaultStdioHarnessFiles,O as formatValidationReport,k as formatZodIssues,f as isDefaultStdioHarnessSource,c as judgeAgainstExpectations,C as languageIdToSourceCodeGrammar,l as mergeComparisonOutputFiles,j as mergeSubmissionPeriods,E as normalizeCodeRule,p as parseArgs,h as parseCsv,g as parseCsvRecords,i as printTestCaseResult,D as problemMarkdownFrontMatterSchema,_ as readTestCases,v as removeCommentsAndStringsInSourceCode,y as removeCommentsInSourceCode,A as reportDuplicateIds,M as reportExcessChatMarkers,N as reportForeignProblemCourseIds,x as runCommandInTemporaryPackageManagerProject,S as runPythonInTemporaryUvProject,w as startHttpServer,n as testCaseResultSchema,I as validateContestFile,L as validateCourseDirectory,P as validateMaterialFile,R as validateProblemDirectory,F as validateSubmissionPeriods};
1
+ import{compareStdoutAsSpaceSeparatedTokens as e}from"./helpers/compareStdoutAsSpaceSeparatedTokens.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{MAX_COMPARED_FILE_BYTES as a,MAX_REPORTED_FILE_BYTES as o,compareExpectedOutputFiles as s,judgeAgainstExpectations as c,mergeComparisonOutputFiles as l}from"./helpers/compareExpectedOutputFiles.js";import{HARNESS_FILE_PRESETS as u,findDefaultStdioHarnessFiles as d,isDefaultStdioHarnessSource as f}from"./helpers/defaultStdioHarness.js";import{parseArgs as p}from"./helpers/parseArgs.js";import{CsvParseError as m,parseCsv as h,parseCsvRecords as g}from"./helpers/parseCsv.js";import{readTestCases as _}from"./helpers/readTestCases.js";import{removeCommentsAndStringsInSourceCode as v,removeCommentsInSourceCode as y}from"./helpers/removeCommentsInSourceCode.js";import{copyPackageManagerProjectFiles as b,runCommandInTemporaryPackageManagerProject as x}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as S}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as C}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as w}from"./helpers/startHttpServer.js";import{codeRuleSchema as T,normalizeCodeRule as E,problemMarkdownFrontMatterSchema as D}from"./types/problem.js";import{parseFrontmatter as O}from"./learningMaterial/frontmatter.js";import{formatValidationReport as k,formatZodIssues as A,reportDuplicateIds as j}from"./learningMaterial/validationResult.js";import{mergeSubmissionPeriods as M,reportExcessChatMarkers as N,reportForeignProblemCourseIds as P,validateMaterialFile as F,validateSubmissionPeriods as I}from"./learningMaterial/validateMaterial.js";import{validateContestFile as L}from"./learningMaterial/validateContest.js";import{validateCourseDirectory as R}from"./learningMaterial/validateCourse.js";import{validateProblemDirectory as z}from"./learningMaterial/validateProblem.js";import{DecisionCode as B}from"./types/decisionCode.js";export{m as CsvParseError,B as DecisionCode,u as HARNESS_FILE_PRESETS,a as MAX_COMPARED_FILE_BYTES,o as MAX_REPORTED_FILE_BYTES,t as TEST_CASE_RESULT_PREFIX,T as codeRuleSchema,s as compareExpectedOutputFiles,e as compareStdoutAsSpaceSeparatedTokens,b as copyPackageManagerProjectFiles,r as encodeFileForTestCaseResult,d as findDefaultStdioHarnessFiles,k as formatValidationReport,A as formatZodIssues,f as isDefaultStdioHarnessSource,c as judgeAgainstExpectations,C as languageIdToSourceCodeGrammar,l as mergeComparisonOutputFiles,M as mergeSubmissionPeriods,E as normalizeCodeRule,p as parseArgs,h as parseCsv,g as parseCsvRecords,O as parseFrontmatter,i as printTestCaseResult,D as problemMarkdownFrontMatterSchema,_ as readTestCases,v as removeCommentsAndStringsInSourceCode,y as removeCommentsInSourceCode,j as reportDuplicateIds,N as reportExcessChatMarkers,P as reportForeignProblemCourseIds,x as runCommandInTemporaryPackageManagerProject,S as runPythonInTemporaryUvProject,w as startHttpServer,n as testCaseResultSchema,L as validateContestFile,R as validateCourseDirectory,F as validateMaterialFile,z as validateProblemDirectory,I as validateSubmissionPeriods};
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/printTestCaseResult.cjs"),n=require("../helpers/parseArgs.cjs"),r=require("../types/decisionCode.cjs");let i=require("node:fs");i=e.__toESM(i,1);let a=require("node:path");a=e.__toESM(a,1);let o=require("zod"),s=require("node:os"),c=require("node:child_process"),l=require("node:http");l=e.__toESM(l,1);let u=require("node:timers/promises");const d=`judge`,f=5e4,p=`http://localhost:59000`,m=new Set([`.java`,`.jsp`,`.html`,`.htm`,`.xml`,`.js`,`.css`]);async function h(e){if(process.argv.includes(`--evaluate`)){try{await e.evaluate()}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exitCode=1}return}let{cwd:o}=n.parseArgs(process.argv);if(!o)throw Error(`cwd argument required`);let c=`test1`,l=await y(o,e.forbiddenTexts??[]);if(l){t.printTestCaseResult({testCaseId:c,...l});return}let u=await i.default.promises.mkdtemp(a.default.join((0,s.tmpdir)(),`tomcat_judge_`)),d=a.default.join(u,`build`),f,p;try{await i.default.promises.mkdir(d,{recursive:!0}),await g(d,o,e);let n=S(d);if(n){t.printTestCaseResult({testCaseId:c,...n});return}p=M(),f=await w(p,u),await k(p,f),await T(d,f),await E(p,f);let r=D(d,e.problemDirectoryPath);t.printTestCaseResult({testCaseId:c,...r})}catch(e){t.printTestCaseResult({testCaseId:c,decisionCode:r.DecisionCode.JUDGE_NOT_AVAILABLE,stderr:e instanceof Error?e.message:String(e)})}finally{try{p&&f&&await k(p,f)}catch{}await i.default.promises.rm(u,{recursive:!0,force:!0})}}async function g(e,t,n){await i.default.promises.mkdir(a.default.join(e,`src/main/java`),{recursive:!0}),await i.default.promises.mkdir(a.default.join(e,`src/main/webapp/WEB-INF/jsp`),{recursive:!0}),await i.default.promises.copyFile(a.default.join(n.problemDirectoryPath,`pom.xml`),a.default.join(e,`pom.xml`));let r=await b(t);for(let o of r){let r=a.default.join(t,o),s=await _(e,o,r,n.jspDirectory);await i.default.promises.mkdir(a.default.dirname(s),{recursive:!0}),await i.default.promises.copyFile(r,s)}}async function _(e,t,n,r){let i=t.replaceAll(`\\`,`/`);if(i.startsWith(`src/main/`))return a.default.join(e,i);if(i.startsWith(`WEB-INF/`)||i.startsWith(`META-INF/`))return a.default.join(e,`src/main/webapp`,i);let o=a.default.extname(t).toLowerCase();return o===`.java`?a.default.join(e,`src/main/java`,await v(n,t)):o===`.jsp`?a.default.join(e,`src/main/webapp`,r,a.default.basename(t)):a.default.join(e,`src/main/webapp`,i)}async function v(e,t){let n=await i.default.promises.readFile(e,`utf8`),r=/^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(n);return r?.[1]?a.default.join(r[1].replaceAll(`.`,`/`),a.default.basename(t)):t}async function y(e,t){let n=[];for(let r of await b(e)){if(!m.has(a.default.extname(r).toLowerCase()))continue;let o=await i.default.promises.readFile(a.default.join(e,r),`utf8`);for(let e of t)o.includes(e)&&n.push({path:r,pattern:e})}if(n.length!==0)return{decisionCode:r.DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
2
+ ソースコードを修正してから再度提出してください。
3
+
4
+ | ファイル | 禁止パターン |
5
+ | -------- | ------------ |
6
+ `+n.map(e=>`| \`${e.path}\` | \`${e.pattern}\` |`).join(`
7
+ `)}}async function b(e){let t=[];return await x(e,``,t),t.toSorted()}async function x(e,t,n){let r=t?a.default.join(e,t):e,o=await i.default.promises.readdir(r,{withFileTypes:!0});for(let r of o){let i=t?a.default.join(t,r.name):r.name;if(r.isDirectory()){await x(e,i,n);continue}r.isFile()&&n.push(i)}}function S(e){let t=O([`mvn`,`clean`,`package`,`--quiet`,`--batch-mode`,`--offline`],e,60);if(t.timeSeconds>60)return{decisionCode:r.DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,stderr:F(t.stderr),stdout:F(t.stdout),exitStatus:t.status,timeSeconds:t.timeSeconds,memoryBytes:t.memoryBytes};if(t.status!==0)return{decisionCode:r.DecisionCode.BUILD_ERROR,stderr:C(t.stdout,t.stderr),stdout:F(t.stdout),exitStatus:t.status,timeSeconds:t.timeSeconds,memoryBytes:t.memoryBytes}}function C(e,t){return F(`${e}
8
+ ${t}`.trim())}async function w(e,t){let n=a.default.join(t,`catalina-base`);await i.default.promises.cp(a.default.join(e,`conf`),a.default.join(n,`conf`),{recursive:!0,dereference:!0});for(let e of[`logs`,`temp`,`webapps`,`work`])await i.default.promises.mkdir(a.default.join(n,e),{recursive:!0});let r=a.default.join(n,`conf`,`server.xml`),o=await i.default.promises.readFile(r,`utf8`);return await i.default.promises.writeFile(r,o.replaceAll(`port="8080"`,`port="59000"`),`utf8`),n}async function T(e,t){let n=a.default.join(e,`target`,`${d}.war`),r=a.default.join(t,`webapps`),o=a.default.join(r,d),s=a.default.join(r,`${d}.war`);await i.default.promises.mkdir(r,{recursive:!0}),await i.default.promises.rm(o,{recursive:!0,force:!0}),await i.default.promises.rm(s,{force:!0}),await i.default.promises.copyFile(n,s)}async function E(e,t){let n=O([a.default.join(e,`bin`,`startup.sh`)],void 0,10,{...process.env,CATALINA_BASE:t,CATALINA_HOME:e});if(n.status!==0)throw Error(`Failed to start Tomcat: ${n.stderr.trim()||n.stdout.trim()}`);if(!await A(!0,1e4))throw Error(`Tomcat failed to start within 10 seconds`)}function D(e,t){let n=O([`bun`,`run`,`./judge.ts`,`--evaluate`],t,30,{...process.env,WORKING_DIRECTORY_PATH:e}),i=P(a.default.join(e,`__SCREENSHOTS.json`));return n.timeSeconds>30?{decisionCode:r.DecisionCode.TIME_LIMIT_EXCEEDED,stderr:F(n.stderr),stdout:F(n.stdout),exitStatus:n.status,timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes,outputFiles:i}:n.stdout.length>f||n.stderr.length>f?{decisionCode:r.DecisionCode.OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:F(n.stderr),stdout:F(n.stdout),exitStatus:n.status,timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes,outputFiles:i}:n.status===0?{decisionCode:r.DecisionCode.ACCEPTED,stderr:F(n.stderr)||void 0,stdout:F(n.stdout)||void 0,exitStatus:n.status,timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes,outputFiles:i}:{decisionCode:r.DecisionCode.WRONG_ANSWER,stderr:F(n.stderr),stdout:F(n.stdout),exitStatus:n.status,timeSeconds:n.timeSeconds,memoryBytes:n.memoryBytes,outputFiles:i}}function O(e,t,n,r=process.env){let i=Date.now(),a=process.platform===`darwin`?`gtime`:`/usr/bin/time`,o=(0,c.spawnSync)(`timeout`,[n.toFixed(3),a,`--format`,`%e %M`,...e],{cwd:t,env:r,encoding:`utf8`}),s=(Date.now()-i)/1e3,l=[o.stderr,o.error?.message].filter(Boolean).join(`
9
+ `),u=/(?:^|\n)(\d+(?:\.\d+)?) (\d+)\s*$/u.exec(l),d=u?l.slice(0,u.index):l,f=Number(u?.[1])||s,p=1024*Number(u?.[2]??0);return{status:o.status===124?0:o.status??void 0,stdout:o.stdout,stderr:d,timeSeconds:o.status===124?n+.001:f,memoryBytes:p}}async function k(e,t){await j()&&(O([a.default.join(e,`bin`,`catalina.sh`),`stop`],void 0,10,{...process.env,CATALINA_BASE:t,CATALINA_HOME:e}),await A(!1,1e4))}async function A(e,t){let n=Date.now();for(;Date.now()-n<t;){if(await j()===e)return!0;await(0,u.setTimeout)(200)}return await j()===e}async function j(){return await new Promise(e=>{let t=l.default.get(p,t=>{t.resume();let n=t.statusCode??0;e(n>=200&&n<300||n===404)});t.on(`error`,()=>e(!1)),t.setTimeout(1e3,()=>t.destroy())})}function M(){let e=process.env.CATALINA_HOME;if(!e)throw Error(`CATALINA_HOME is not set`);return N(e),a.default.resolve(e)}function N(e){if(!i.default.existsSync(a.default.join(e,`bin`,`catalina.sh`))||!i.default.existsSync(a.default.join(e,`conf`,`server.xml`)))throw Error(`Invalid CATALINA_HOME path`)}function P(e){if(!i.default.existsSync(e))return;let t=JSON.parse(i.default.readFileSync(e,`utf8`)),n=o.z.array(o.z.unknown()).safeParse(t);if(!n.success)return;let r=o.z.object({path:o.z.string(),data:o.z.string(),encoding:o.z.literal(`base64`).optional()});return n.data.flatMap(e=>{let t=r.safeParse(e);return t.success?[t.data]:[]})}function F(e){return e.slice(0,f)}function I(e){return`${p}/${d}${e.startsWith(`/`)?e:`/${e}`}`}exports.buildTomcatUrl=I,exports.tomcatJudgePreset=h;
10
+ //# sourceMappingURL=tomcat.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tomcat.cjs","names":["parseArgs","fs","path","tmpdir","DecisionCode","spawnSync","sleep","http","z"],"sources":["../../src/presets/tomcat.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport http from 'node:http';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { DecisionCode, parseArgs, printTestCaseResult, type TestCaseResult } from '../index.js';\nimport { z } from 'zod';\n\nexport interface TomcatJudgePresetOptions {\n problemDirectoryPath: string;\n jspDirectory: '' | 'WEB-INF/jsp';\n forbiddenTexts?: readonly string[];\n evaluate: () => Promise<void>;\n}\n\ntype JudgeResult = Omit<TestCaseResult, 'testCaseId'>;\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\ninterface CommandResult {\n status: number | undefined;\n stdout: string;\n stderr: string;\n timeSeconds: number;\n memoryBytes: number;\n}\nconst APPLICATION_NAME = 'judge';\nconst BUILD_TIMEOUT_SECONDS = 60;\nconst EVALUATE_TIMEOUT_SECONDS = 30;\nconst MAX_OUTPUT_LENGTH = 50_000;\nconst TOMCAT_BASE_URL = 'http://localhost:59000';\nconst SOURCE_FILE_EXTENSIONS = new Set(['.java', '.jsp', '.html', '.htm', '.xml', '.js', '.css']);\n\nexport async function tomcatJudgePreset(options: TomcatJudgePresetOptions): Promise<void> {\n if (process.argv.includes('--evaluate')) {\n try {\n await options.evaluate();\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n }\n return;\n }\n\n const { cwd: submissionDir } = parseArgs(process.argv);\n if (!submissionDir) throw new Error('cwd argument required');\n const testCaseId = 'test1';\n const staticAnalysisResult = await judgeByStaticAnalysis(submissionDir, options.forbiddenTexts ?? []);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId, ...staticAnalysisResult });\n return;\n }\n\n const runRootDir = await fs.promises.mkdtemp(path.join(tmpdir(), 'tomcat_judge_'));\n const buildDir = path.join(runRootDir, 'build');\n let catalinaBaseDir: string | undefined;\n let catalinaHome: string | undefined;\n\n try {\n await fs.promises.mkdir(buildDir, { recursive: true });\n await prepareBuildDirectory(buildDir, submissionDir, options);\n\n const buildResult = buildWithMaven(buildDir);\n if (buildResult) {\n printTestCaseResult({ testCaseId, ...buildResult });\n return;\n }\n\n catalinaHome = getCatalinaHome();\n catalinaBaseDir = await createCatalinaBaseDir(catalinaHome, runRootDir);\n await ensureTomcatStopped(catalinaHome, catalinaBaseDir);\n await deployWarFile(buildDir, catalinaBaseDir);\n await startTomcat(catalinaHome, catalinaBaseDir);\n\n const judgeResult = runJudgeScript(buildDir, options.problemDirectoryPath);\n printTestCaseResult({ testCaseId, ...judgeResult });\n } catch (error) {\n printTestCaseResult({\n testCaseId,\n decisionCode: DecisionCode.JUDGE_NOT_AVAILABLE,\n stderr: error instanceof Error ? error.message : String(error),\n });\n } finally {\n try {\n if (catalinaHome && catalinaBaseDir) {\n await ensureTomcatStopped(catalinaHome, catalinaBaseDir);\n }\n } catch {\n // 判定結果を優先し、後始末の失敗は握りつぶす。\n }\n await fs.promises.rm(runRootDir, { recursive: true, force: true });\n }\n}\n\nasync function prepareBuildDirectory(\n buildDir: string,\n submissionDir: string,\n options: TomcatJudgePresetOptions\n): Promise<void> {\n await fs.promises.mkdir(path.join(buildDir, 'src/main/java'), { recursive: true });\n await fs.promises.mkdir(path.join(buildDir, 'src/main/webapp/WEB-INF/jsp'), { recursive: true });\n await fs.promises.copyFile(path.join(options.problemDirectoryPath, 'pom.xml'), path.join(buildDir, 'pom.xml'));\n\n const submissionFiles = await listSubmissionFiles(submissionDir);\n for (const relativePath of submissionFiles) {\n const sourcePath = path.join(submissionDir, relativePath);\n const targetPath = await resolveTargetPath(buildDir, relativePath, sourcePath, options.jspDirectory);\n await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });\n await fs.promises.copyFile(sourcePath, targetPath);\n }\n}\n\nasync function resolveTargetPath(\n buildDir: string,\n relativePath: string,\n sourcePath: string,\n jspDirectory: string\n): Promise<string> {\n const normalizedRelativePath = relativePath.replaceAll('\\\\', '/');\n if (normalizedRelativePath.startsWith('src/main/')) {\n return path.join(buildDir, normalizedRelativePath);\n }\n if (normalizedRelativePath.startsWith('WEB-INF/') || normalizedRelativePath.startsWith('META-INF/')) {\n return path.join(buildDir, 'src/main/webapp', normalizedRelativePath);\n }\n\n const extension = path.extname(relativePath).toLowerCase();\n if (extension === '.java') {\n return path.join(buildDir, 'src/main/java', await resolveJavaRelativePath(sourcePath, relativePath));\n }\n if (extension === '.jsp') {\n return path.join(buildDir, 'src/main/webapp', jspDirectory, path.basename(relativePath));\n }\n\n return path.join(buildDir, 'src/main/webapp', normalizedRelativePath);\n}\n\nasync 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\nasync function judgeByStaticAnalysis(\n submissionDir: string,\n forbiddenTexts: readonly string[]\n): Promise<Pick<JudgeResult, 'decisionCode' | 'feedbackMarkdown'> | undefined> {\n const forbiddenMatches: { path: string; pattern: string }[] = [];\n for (const relativePath of await listSubmissionFiles(submissionDir)) {\n if (!SOURCE_FILE_EXTENSIONS.has(path.extname(relativePath).toLowerCase())) {\n continue;\n }\n\n const sourceCode = await fs.promises.readFile(path.join(submissionDir, relativePath), 'utf8');\n for (const forbiddenText of forbiddenTexts) {\n if (sourceCode.includes(forbiddenText)) {\n forbiddenMatches.push({ path: relativePath, pattern: forbiddenText });\n }\n }\n }\n\n if (forbiddenMatches.length === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,\n feedbackMarkdown:\n 'ソースコード中に禁止された文字列が含まれています。\\nソースコードを修正してから再度提出してください。\\n\\n| ファイル | 禁止パターン |\\n| -------- | ------------ |\\n' +\n forbiddenMatches.map((match) => `| \\`${match.path}\\` | \\`${match.pattern}\\` |`).join('\\n'),\n };\n}\n\nasync 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\nfunction buildWithMaven(buildDir: string): JudgeResult | undefined {\n const commandResult = runCommand(\n ['mvn', 'clean', 'package', '--quiet', '--batch-mode', '--offline'],\n buildDir,\n BUILD_TIMEOUT_SECONDS\n );\n if (commandResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n };\n }\n if (commandResult.status === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: formatMavenBuildError(commandResult.stdout, commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n };\n}\n\nfunction formatMavenBuildError(stdout: string, stderr: string): string {\n return truncateOutput(\n `${stdout}\n${stderr}`.trim()\n );\n}\n\nasync function createCatalinaBaseDir(catalinaHome: string, runRootDir: string): Promise<string> {\n const catalinaBaseDir = path.join(runRootDir, 'catalina-base');\n await fs.promises.cp(path.join(catalinaHome, 'conf'), path.join(catalinaBaseDir, 'conf'), {\n recursive: true,\n dereference: true,\n });\n for (const directoryName of ['logs', 'temp', 'webapps', 'work']) {\n await fs.promises.mkdir(path.join(catalinaBaseDir, directoryName), { recursive: true });\n }\n\n const serverXmlPath = path.join(catalinaBaseDir, 'conf', 'server.xml');\n const serverXml = await fs.promises.readFile(serverXmlPath, 'utf8');\n await fs.promises.writeFile(serverXmlPath, serverXml.replaceAll('port=\"8080\"', 'port=\"59000\"'), 'utf8');\n return catalinaBaseDir;\n}\n\nasync function deployWarFile(buildDir: string, catalinaBaseDir: string): Promise<void> {\n const warFilePath = path.join(buildDir, 'target', `${APPLICATION_NAME}.war`);\n const webappsDir = path.join(catalinaBaseDir, 'webapps');\n const deployedDirectoryPath = path.join(webappsDir, APPLICATION_NAME);\n const deployedWarPath = path.join(webappsDir, `${APPLICATION_NAME}.war`);\n await fs.promises.mkdir(webappsDir, { recursive: true });\n await fs.promises.rm(deployedDirectoryPath, { recursive: true, force: true });\n await fs.promises.rm(deployedWarPath, { force: true });\n await fs.promises.copyFile(warFilePath, deployedWarPath);\n}\n\nasync function startTomcat(catalinaHome: string, catalinaBaseDir: string): Promise<void> {\n const startupScriptPath = path.join(catalinaHome, 'bin', 'startup.sh');\n const startupResult = runCommand([startupScriptPath], undefined, 10, {\n ...process.env,\n CATALINA_BASE: catalinaBaseDir,\n CATALINA_HOME: catalinaHome,\n });\n if (startupResult.status !== 0) {\n throw new Error(`Failed to start Tomcat: ${startupResult.stderr.trim() || startupResult.stdout.trim()}`);\n }\n\n const started = await waitForTomcatState(true, 10_000);\n if (!started) {\n throw new Error('Tomcat failed to start within 10 seconds');\n }\n}\n\nfunction runJudgeScript(buildDir: string, problemDirectoryPath: string): JudgeResult {\n const commandResult = runCommand(\n ['bun', 'run', './judge.ts', '--evaluate'],\n problemDirectoryPath,\n EVALUATE_TIMEOUT_SECONDS,\n {\n ...process.env,\n WORKING_DIRECTORY_PATH: buildDir,\n }\n );\n\n const outputFiles = readOutputFiles(path.join(buildDir, '__SCREENSHOTS.json'));\n if (commandResult.timeSeconds > EVALUATE_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n }\n if (commandResult.stdout.length > MAX_OUTPUT_LENGTH || commandResult.stderr.length > MAX_OUTPUT_LENGTH) {\n return {\n decisionCode: DecisionCode.OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n }\n if (commandResult.status !== 0) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n }\n\n return {\n decisionCode: DecisionCode.ACCEPTED,\n stderr: truncateOutput(commandResult.stderr) || undefined,\n stdout: truncateOutput(commandResult.stdout) || undefined,\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n}\n\nfunction runCommand(\n command: readonly [string, ...string[]],\n cwd: string | undefined,\n timeoutSeconds: number,\n env: NodeJS.ProcessEnv = process.env\n): CommandResult {\n const startAt = Date.now();\n const timeCommand = process.platform === 'darwin' ? 'gtime' : '/usr/bin/time';\n // Hosts provide GNU timeout on PATH; Homebrew coreutils also installs the unprefixed command.\n const timedResult = spawnSync('timeout', [timeoutSeconds.toFixed(3), timeCommand, '--format', '%e %M', ...command], {\n cwd,\n env,\n encoding: 'utf8',\n });\n const elapsedSeconds = (Date.now() - startAt) / 1000;\n const stderrWithError = [timedResult.stderr, timedResult.error?.message].filter(Boolean).join('\\n');\n const memoryMatch = /(?:^|\\n)(\\d+(?:\\.\\d+)?) (\\d+)\\s*$/u.exec(stderrWithError);\n const stderr = memoryMatch ? stderrWithError.slice(0, memoryMatch.index) : stderrWithError;\n const timeSeconds = Number(memoryMatch?.[1]) || elapsedSeconds;\n const memoryBytes = 1024 * Number(memoryMatch?.[2] ?? 0);\n\n return {\n status: timedResult.status === 124 ? 0 : (timedResult.status ?? undefined),\n stdout: timedResult.stdout,\n stderr,\n timeSeconds: timedResult.status === 124 ? timeoutSeconds + 0.001 : timeSeconds,\n memoryBytes,\n };\n}\n\nasync function ensureTomcatStopped(catalinaHome: string, catalinaBaseDir: string): Promise<void> {\n if (!(await isTomcatRunning())) {\n return;\n }\n\n runCommand([path.join(catalinaHome, 'bin', 'catalina.sh'), 'stop'], undefined, 10, {\n ...process.env,\n CATALINA_BASE: catalinaBaseDir,\n CATALINA_HOME: catalinaHome,\n });\n await waitForTomcatState(false, 10_000);\n}\n\nasync function waitForTomcatState(expectedRunning: boolean, timeoutMs: number): Promise<boolean> {\n const startedAt = Date.now();\n while (Date.now() - startedAt < timeoutMs) {\n if ((await isTomcatRunning()) === expectedRunning) {\n return true;\n }\n await sleep(200);\n }\n return (await isTomcatRunning()) === expectedRunning;\n}\n\nasync function isTomcatRunning(): Promise<boolean> {\n return await new Promise<boolean>((resolve) => {\n const request = http.get(TOMCAT_BASE_URL, (response) => {\n response.resume();\n const status = response.statusCode ?? 0;\n resolve((status >= 200 && status < 300) || status === 404);\n });\n request.on('error', () => resolve(false));\n request.setTimeout(1000, () => request.destroy());\n });\n}\n\nfunction getCatalinaHome(): string {\n const catalinaHome = process.env.CATALINA_HOME;\n if (!catalinaHome) {\n throw new Error('CATALINA_HOME is not set');\n }\n\n validateCatalinaHome(catalinaHome);\n return path.resolve(catalinaHome);\n}\n\nfunction validateCatalinaHome(catalinaHome: string): void {\n if (\n !fs.existsSync(path.join(catalinaHome, 'bin', 'catalina.sh')) ||\n !fs.existsSync(path.join(catalinaHome, 'conf', 'server.xml'))\n ) {\n throw new Error('Invalid CATALINA_HOME path');\n }\n}\n\nfunction 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\nfunction truncateOutput(value: string): string {\n return value.slice(0, MAX_OUTPUT_LENGTH);\n}\n\nexport function buildTomcatUrl(urlPath: string): string {\n return `${TOMCAT_BASE_URL}/${APPLICATION_NAME}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export interface TomcatJudgePresetOptions {
2
+ problemDirectoryPath: string;
3
+ jspDirectory: '' | 'WEB-INF/jsp';
4
+ forbiddenTexts?: readonly string[];
5
+ evaluate: () => Promise<void>;
6
+ }
7
+ export declare function tomcatJudgePreset(options: TomcatJudgePresetOptions): Promise<void>;
8
+ export declare function buildTomcatUrl(urlPath: string): string;
@@ -0,0 +1,10 @@
1
+ import{printTestCaseResult as e}from"../helpers/printTestCaseResult.js";import{parseArgs as t}from"../helpers/parseArgs.js";import{DecisionCode as n}from"../types/decisionCode.js";import r from"node:fs";import i from"node:path";import{z as a}from"zod";import{tmpdir as o}from"node:os";import{spawnSync as s}from"node:child_process";import c from"node:http";import{setTimeout as l}from"node:timers/promises";const u=`judge`,d=5e4,f=`http://localhost:59000`,p=new Set([`.java`,`.jsp`,`.html`,`.htm`,`.xml`,`.js`,`.css`]);async function m(a){if(process.argv.includes(`--evaluate`)){try{await a.evaluate()}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exitCode=1}return}let{cwd:s}=t(process.argv);if(!s)throw Error(`cwd argument required`);let c=`test1`,l=await v(s,a.forbiddenTexts??[]);if(l){e({testCaseId:c,...l});return}let u=await r.promises.mkdtemp(i.join(o(),`tomcat_judge_`)),d=i.join(u,`build`),f,p;try{await r.promises.mkdir(d,{recursive:!0}),await h(d,s,a);let t=x(d);if(t){e({testCaseId:c,...t});return}p=j(),f=await C(p,u),await O(p,f),await w(d,f),await T(p,f);let n=E(d,a.problemDirectoryPath);e({testCaseId:c,...n})}catch(t){e({testCaseId:c,decisionCode:n.JUDGE_NOT_AVAILABLE,stderr:t instanceof Error?t.message:String(t)})}finally{try{p&&f&&await O(p,f)}catch{}await r.promises.rm(u,{recursive:!0,force:!0})}}async function h(e,t,n){await r.promises.mkdir(i.join(e,`src/main/java`),{recursive:!0}),await r.promises.mkdir(i.join(e,`src/main/webapp/WEB-INF/jsp`),{recursive:!0}),await r.promises.copyFile(i.join(n.problemDirectoryPath,`pom.xml`),i.join(e,`pom.xml`));let a=await y(t);for(let o of a){let a=i.join(t,o),s=await g(e,o,a,n.jspDirectory);await r.promises.mkdir(i.dirname(s),{recursive:!0}),await r.promises.copyFile(a,s)}}async function g(e,t,n,r){let a=t.replaceAll(`\\`,`/`);if(a.startsWith(`src/main/`))return i.join(e,a);if(a.startsWith(`WEB-INF/`)||a.startsWith(`META-INF/`))return i.join(e,`src/main/webapp`,a);let o=i.extname(t).toLowerCase();return o===`.java`?i.join(e,`src/main/java`,await _(n,t)):o===`.jsp`?i.join(e,`src/main/webapp`,r,i.basename(t)):i.join(e,`src/main/webapp`,a)}async function _(e,t){let n=await r.promises.readFile(e,`utf8`),a=/^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(n);return a?.[1]?i.join(a[1].replaceAll(`.`,`/`),i.basename(t)):t}async function v(e,t){let a=[];for(let n of await y(e)){if(!p.has(i.extname(n).toLowerCase()))continue;let o=await r.promises.readFile(i.join(e,n),`utf8`);for(let e of t)o.includes(e)&&a.push({path:n,pattern:e})}if(a.length!==0)return{decisionCode:n.FORBIDDEN_PATTERNS_IN_CODE_ERROR,feedbackMarkdown:`ソースコード中に禁止された文字列が含まれています。
2
+ ソースコードを修正してから再度提出してください。
3
+
4
+ | ファイル | 禁止パターン |
5
+ | -------- | ------------ |
6
+ `+a.map(e=>`| \`${e.path}\` | \`${e.pattern}\` |`).join(`
7
+ `)}}async function y(e){let t=[];return await b(e,``,t),t.toSorted()}async function b(e,t,n){let a=t?i.join(e,t):e,o=await r.promises.readdir(a,{withFileTypes:!0});for(let r of o){let a=t?i.join(t,r.name):r.name;if(r.isDirectory()){await b(e,a,n);continue}r.isFile()&&n.push(a)}}function x(e){let t=D([`mvn`,`clean`,`package`,`--quiet`,`--batch-mode`,`--offline`],e,60);if(t.timeSeconds>60)return{decisionCode:n.BUILD_TIME_LIMIT_EXCEEDED,stderr:P(t.stderr),stdout:P(t.stdout),exitStatus:t.status,timeSeconds:t.timeSeconds,memoryBytes:t.memoryBytes};if(t.status!==0)return{decisionCode:n.BUILD_ERROR,stderr:S(t.stdout,t.stderr),stdout:P(t.stdout),exitStatus:t.status,timeSeconds:t.timeSeconds,memoryBytes:t.memoryBytes}}function S(e,t){return P(`${e}
8
+ ${t}`.trim())}async function C(e,t){let n=i.join(t,`catalina-base`);await r.promises.cp(i.join(e,`conf`),i.join(n,`conf`),{recursive:!0,dereference:!0});for(let e of[`logs`,`temp`,`webapps`,`work`])await r.promises.mkdir(i.join(n,e),{recursive:!0});let a=i.join(n,`conf`,`server.xml`),o=await r.promises.readFile(a,`utf8`);return await r.promises.writeFile(a,o.replaceAll(`port="8080"`,`port="59000"`),`utf8`),n}async function w(e,t){let n=i.join(e,`target`,`${u}.war`),a=i.join(t,`webapps`),o=i.join(a,u),s=i.join(a,`${u}.war`);await r.promises.mkdir(a,{recursive:!0}),await r.promises.rm(o,{recursive:!0,force:!0}),await r.promises.rm(s,{force:!0}),await r.promises.copyFile(n,s)}async function T(e,t){let n=D([i.join(e,`bin`,`startup.sh`)],void 0,10,{...process.env,CATALINA_BASE:t,CATALINA_HOME:e});if(n.status!==0)throw Error(`Failed to start Tomcat: ${n.stderr.trim()||n.stdout.trim()}`);if(!await k(!0,1e4))throw Error(`Tomcat failed to start within 10 seconds`)}function E(e,t){let r=D([`bun`,`run`,`./judge.ts`,`--evaluate`],t,30,{...process.env,WORKING_DIRECTORY_PATH:e}),a=N(i.join(e,`__SCREENSHOTS.json`));return r.timeSeconds>30?{decisionCode:n.TIME_LIMIT_EXCEEDED,stderr:P(r.stderr),stdout:P(r.stdout),exitStatus:r.status,timeSeconds:r.timeSeconds,memoryBytes:r.memoryBytes,outputFiles:a}:r.stdout.length>d||r.stderr.length>d?{decisionCode:n.OUTPUT_SIZE_LIMIT_EXCEEDED,stderr:P(r.stderr),stdout:P(r.stdout),exitStatus:r.status,timeSeconds:r.timeSeconds,memoryBytes:r.memoryBytes,outputFiles:a}:r.status===0?{decisionCode:n.ACCEPTED,stderr:P(r.stderr)||void 0,stdout:P(r.stdout)||void 0,exitStatus:r.status,timeSeconds:r.timeSeconds,memoryBytes:r.memoryBytes,outputFiles:a}:{decisionCode:n.WRONG_ANSWER,stderr:P(r.stderr),stdout:P(r.stdout),exitStatus:r.status,timeSeconds:r.timeSeconds,memoryBytes:r.memoryBytes,outputFiles:a}}function D(e,t,n,r=process.env){let i=Date.now(),a=process.platform===`darwin`?`gtime`:`/usr/bin/time`,o=s(`timeout`,[n.toFixed(3),a,`--format`,`%e %M`,...e],{cwd:t,env:r,encoding:`utf8`}),c=(Date.now()-i)/1e3,l=[o.stderr,o.error?.message].filter(Boolean).join(`
9
+ `),u=/(?:^|\n)(\d+(?:\.\d+)?) (\d+)\s*$/u.exec(l),d=u?l.slice(0,u.index):l,f=Number(u?.[1])||c,p=1024*Number(u?.[2]??0);return{status:o.status===124?0:o.status??void 0,stdout:o.stdout,stderr:d,timeSeconds:o.status===124?n+.001:f,memoryBytes:p}}async function O(e,t){await A()&&(D([i.join(e,`bin`,`catalina.sh`),`stop`],void 0,10,{...process.env,CATALINA_BASE:t,CATALINA_HOME:e}),await k(!1,1e4))}async function k(e,t){let n=Date.now();for(;Date.now()-n<t;){if(await A()===e)return!0;await l(200)}return await A()===e}async function A(){return await new Promise(e=>{let t=c.get(f,t=>{t.resume();let n=t.statusCode??0;e(n>=200&&n<300||n===404)});t.on(`error`,()=>e(!1)),t.setTimeout(1e3,()=>t.destroy())})}function j(){let e=process.env.CATALINA_HOME;if(!e)throw Error(`CATALINA_HOME is not set`);return M(e),i.resolve(e)}function M(e){if(!r.existsSync(i.join(e,`bin`,`catalina.sh`))||!r.existsSync(i.join(e,`conf`,`server.xml`)))throw Error(`Invalid CATALINA_HOME path`)}function N(e){if(!r.existsSync(e))return;let t=JSON.parse(r.readFileSync(e,`utf8`)),n=a.array(a.unknown()).safeParse(t);if(!n.success)return;let i=a.object({path:a.string(),data:a.string(),encoding:a.literal(`base64`).optional()});return n.data.flatMap(e=>{let t=i.safeParse(e);return t.success?[t.data]:[]})}function P(e){return e.slice(0,d)}function F(e){return`${f}/${u}${e.startsWith(`/`)?e:`/${e}`}`}export{F as buildTomcatUrl,m as tomcatJudgePreset};
10
+ //# sourceMappingURL=tomcat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tomcat.js","names":["sleep"],"sources":["../../src/presets/tomcat.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport http from 'node:http';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { DecisionCode, parseArgs, printTestCaseResult, type TestCaseResult } from '../index.js';\nimport { z } from 'zod';\n\nexport interface TomcatJudgePresetOptions {\n problemDirectoryPath: string;\n jspDirectory: '' | 'WEB-INF/jsp';\n forbiddenTexts?: readonly string[];\n evaluate: () => Promise<void>;\n}\n\ntype JudgeResult = Omit<TestCaseResult, 'testCaseId'>;\ntype OutputFile = NonNullable<TestCaseResult['outputFiles']>[number];\ninterface CommandResult {\n status: number | undefined;\n stdout: string;\n stderr: string;\n timeSeconds: number;\n memoryBytes: number;\n}\nconst APPLICATION_NAME = 'judge';\nconst BUILD_TIMEOUT_SECONDS = 60;\nconst EVALUATE_TIMEOUT_SECONDS = 30;\nconst MAX_OUTPUT_LENGTH = 50_000;\nconst TOMCAT_BASE_URL = 'http://localhost:59000';\nconst SOURCE_FILE_EXTENSIONS = new Set(['.java', '.jsp', '.html', '.htm', '.xml', '.js', '.css']);\n\nexport async function tomcatJudgePreset(options: TomcatJudgePresetOptions): Promise<void> {\n if (process.argv.includes('--evaluate')) {\n try {\n await options.evaluate();\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n }\n return;\n }\n\n const { cwd: submissionDir } = parseArgs(process.argv);\n if (!submissionDir) throw new Error('cwd argument required');\n const testCaseId = 'test1';\n const staticAnalysisResult = await judgeByStaticAnalysis(submissionDir, options.forbiddenTexts ?? []);\n if (staticAnalysisResult) {\n printTestCaseResult({ testCaseId, ...staticAnalysisResult });\n return;\n }\n\n const runRootDir = await fs.promises.mkdtemp(path.join(tmpdir(), 'tomcat_judge_'));\n const buildDir = path.join(runRootDir, 'build');\n let catalinaBaseDir: string | undefined;\n let catalinaHome: string | undefined;\n\n try {\n await fs.promises.mkdir(buildDir, { recursive: true });\n await prepareBuildDirectory(buildDir, submissionDir, options);\n\n const buildResult = buildWithMaven(buildDir);\n if (buildResult) {\n printTestCaseResult({ testCaseId, ...buildResult });\n return;\n }\n\n catalinaHome = getCatalinaHome();\n catalinaBaseDir = await createCatalinaBaseDir(catalinaHome, runRootDir);\n await ensureTomcatStopped(catalinaHome, catalinaBaseDir);\n await deployWarFile(buildDir, catalinaBaseDir);\n await startTomcat(catalinaHome, catalinaBaseDir);\n\n const judgeResult = runJudgeScript(buildDir, options.problemDirectoryPath);\n printTestCaseResult({ testCaseId, ...judgeResult });\n } catch (error) {\n printTestCaseResult({\n testCaseId,\n decisionCode: DecisionCode.JUDGE_NOT_AVAILABLE,\n stderr: error instanceof Error ? error.message : String(error),\n });\n } finally {\n try {\n if (catalinaHome && catalinaBaseDir) {\n await ensureTomcatStopped(catalinaHome, catalinaBaseDir);\n }\n } catch {\n // 判定結果を優先し、後始末の失敗は握りつぶす。\n }\n await fs.promises.rm(runRootDir, { recursive: true, force: true });\n }\n}\n\nasync function prepareBuildDirectory(\n buildDir: string,\n submissionDir: string,\n options: TomcatJudgePresetOptions\n): Promise<void> {\n await fs.promises.mkdir(path.join(buildDir, 'src/main/java'), { recursive: true });\n await fs.promises.mkdir(path.join(buildDir, 'src/main/webapp/WEB-INF/jsp'), { recursive: true });\n await fs.promises.copyFile(path.join(options.problemDirectoryPath, 'pom.xml'), path.join(buildDir, 'pom.xml'));\n\n const submissionFiles = await listSubmissionFiles(submissionDir);\n for (const relativePath of submissionFiles) {\n const sourcePath = path.join(submissionDir, relativePath);\n const targetPath = await resolveTargetPath(buildDir, relativePath, sourcePath, options.jspDirectory);\n await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });\n await fs.promises.copyFile(sourcePath, targetPath);\n }\n}\n\nasync function resolveTargetPath(\n buildDir: string,\n relativePath: string,\n sourcePath: string,\n jspDirectory: string\n): Promise<string> {\n const normalizedRelativePath = relativePath.replaceAll('\\\\', '/');\n if (normalizedRelativePath.startsWith('src/main/')) {\n return path.join(buildDir, normalizedRelativePath);\n }\n if (normalizedRelativePath.startsWith('WEB-INF/') || normalizedRelativePath.startsWith('META-INF/')) {\n return path.join(buildDir, 'src/main/webapp', normalizedRelativePath);\n }\n\n const extension = path.extname(relativePath).toLowerCase();\n if (extension === '.java') {\n return path.join(buildDir, 'src/main/java', await resolveJavaRelativePath(sourcePath, relativePath));\n }\n if (extension === '.jsp') {\n return path.join(buildDir, 'src/main/webapp', jspDirectory, path.basename(relativePath));\n }\n\n return path.join(buildDir, 'src/main/webapp', normalizedRelativePath);\n}\n\nasync 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\nasync function judgeByStaticAnalysis(\n submissionDir: string,\n forbiddenTexts: readonly string[]\n): Promise<Pick<JudgeResult, 'decisionCode' | 'feedbackMarkdown'> | undefined> {\n const forbiddenMatches: { path: string; pattern: string }[] = [];\n for (const relativePath of await listSubmissionFiles(submissionDir)) {\n if (!SOURCE_FILE_EXTENSIONS.has(path.extname(relativePath).toLowerCase())) {\n continue;\n }\n\n const sourceCode = await fs.promises.readFile(path.join(submissionDir, relativePath), 'utf8');\n for (const forbiddenText of forbiddenTexts) {\n if (sourceCode.includes(forbiddenText)) {\n forbiddenMatches.push({ path: relativePath, pattern: forbiddenText });\n }\n }\n }\n\n if (forbiddenMatches.length === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.FORBIDDEN_PATTERNS_IN_CODE_ERROR,\n feedbackMarkdown:\n 'ソースコード中に禁止された文字列が含まれています。\\nソースコードを修正してから再度提出してください。\\n\\n| ファイル | 禁止パターン |\\n| -------- | ------------ |\\n' +\n forbiddenMatches.map((match) => `| \\`${match.path}\\` | \\`${match.pattern}\\` |`).join('\\n'),\n };\n}\n\nasync 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\nfunction buildWithMaven(buildDir: string): JudgeResult | undefined {\n const commandResult = runCommand(\n ['mvn', 'clean', 'package', '--quiet', '--batch-mode', '--offline'],\n buildDir,\n BUILD_TIMEOUT_SECONDS\n );\n if (commandResult.timeSeconds > BUILD_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.BUILD_TIME_LIMIT_EXCEEDED,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n };\n }\n if (commandResult.status === 0) {\n return undefined;\n }\n\n return {\n decisionCode: DecisionCode.BUILD_ERROR,\n stderr: formatMavenBuildError(commandResult.stdout, commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n };\n}\n\nfunction formatMavenBuildError(stdout: string, stderr: string): string {\n return truncateOutput(\n `${stdout}\n${stderr}`.trim()\n );\n}\n\nasync function createCatalinaBaseDir(catalinaHome: string, runRootDir: string): Promise<string> {\n const catalinaBaseDir = path.join(runRootDir, 'catalina-base');\n await fs.promises.cp(path.join(catalinaHome, 'conf'), path.join(catalinaBaseDir, 'conf'), {\n recursive: true,\n dereference: true,\n });\n for (const directoryName of ['logs', 'temp', 'webapps', 'work']) {\n await fs.promises.mkdir(path.join(catalinaBaseDir, directoryName), { recursive: true });\n }\n\n const serverXmlPath = path.join(catalinaBaseDir, 'conf', 'server.xml');\n const serverXml = await fs.promises.readFile(serverXmlPath, 'utf8');\n await fs.promises.writeFile(serverXmlPath, serverXml.replaceAll('port=\"8080\"', 'port=\"59000\"'), 'utf8');\n return catalinaBaseDir;\n}\n\nasync function deployWarFile(buildDir: string, catalinaBaseDir: string): Promise<void> {\n const warFilePath = path.join(buildDir, 'target', `${APPLICATION_NAME}.war`);\n const webappsDir = path.join(catalinaBaseDir, 'webapps');\n const deployedDirectoryPath = path.join(webappsDir, APPLICATION_NAME);\n const deployedWarPath = path.join(webappsDir, `${APPLICATION_NAME}.war`);\n await fs.promises.mkdir(webappsDir, { recursive: true });\n await fs.promises.rm(deployedDirectoryPath, { recursive: true, force: true });\n await fs.promises.rm(deployedWarPath, { force: true });\n await fs.promises.copyFile(warFilePath, deployedWarPath);\n}\n\nasync function startTomcat(catalinaHome: string, catalinaBaseDir: string): Promise<void> {\n const startupScriptPath = path.join(catalinaHome, 'bin', 'startup.sh');\n const startupResult = runCommand([startupScriptPath], undefined, 10, {\n ...process.env,\n CATALINA_BASE: catalinaBaseDir,\n CATALINA_HOME: catalinaHome,\n });\n if (startupResult.status !== 0) {\n throw new Error(`Failed to start Tomcat: ${startupResult.stderr.trim() || startupResult.stdout.trim()}`);\n }\n\n const started = await waitForTomcatState(true, 10_000);\n if (!started) {\n throw new Error('Tomcat failed to start within 10 seconds');\n }\n}\n\nfunction runJudgeScript(buildDir: string, problemDirectoryPath: string): JudgeResult {\n const commandResult = runCommand(\n ['bun', 'run', './judge.ts', '--evaluate'],\n problemDirectoryPath,\n EVALUATE_TIMEOUT_SECONDS,\n {\n ...process.env,\n WORKING_DIRECTORY_PATH: buildDir,\n }\n );\n\n const outputFiles = readOutputFiles(path.join(buildDir, '__SCREENSHOTS.json'));\n if (commandResult.timeSeconds > EVALUATE_TIMEOUT_SECONDS) {\n return {\n decisionCode: DecisionCode.TIME_LIMIT_EXCEEDED,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n }\n if (commandResult.stdout.length > MAX_OUTPUT_LENGTH || commandResult.stderr.length > MAX_OUTPUT_LENGTH) {\n return {\n decisionCode: DecisionCode.OUTPUT_SIZE_LIMIT_EXCEEDED,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n }\n if (commandResult.status !== 0) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n stderr: truncateOutput(commandResult.stderr),\n stdout: truncateOutput(commandResult.stdout),\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n }\n\n return {\n decisionCode: DecisionCode.ACCEPTED,\n stderr: truncateOutput(commandResult.stderr) || undefined,\n stdout: truncateOutput(commandResult.stdout) || undefined,\n exitStatus: commandResult.status,\n timeSeconds: commandResult.timeSeconds,\n memoryBytes: commandResult.memoryBytes,\n outputFiles,\n };\n}\n\nfunction runCommand(\n command: readonly [string, ...string[]],\n cwd: string | undefined,\n timeoutSeconds: number,\n env: NodeJS.ProcessEnv = process.env\n): CommandResult {\n const startAt = Date.now();\n const timeCommand = process.platform === 'darwin' ? 'gtime' : '/usr/bin/time';\n // Hosts provide GNU timeout on PATH; Homebrew coreutils also installs the unprefixed command.\n const timedResult = spawnSync('timeout', [timeoutSeconds.toFixed(3), timeCommand, '--format', '%e %M', ...command], {\n cwd,\n env,\n encoding: 'utf8',\n });\n const elapsedSeconds = (Date.now() - startAt) / 1000;\n const stderrWithError = [timedResult.stderr, timedResult.error?.message].filter(Boolean).join('\\n');\n const memoryMatch = /(?:^|\\n)(\\d+(?:\\.\\d+)?) (\\d+)\\s*$/u.exec(stderrWithError);\n const stderr = memoryMatch ? stderrWithError.slice(0, memoryMatch.index) : stderrWithError;\n const timeSeconds = Number(memoryMatch?.[1]) || elapsedSeconds;\n const memoryBytes = 1024 * Number(memoryMatch?.[2] ?? 0);\n\n return {\n status: timedResult.status === 124 ? 0 : (timedResult.status ?? undefined),\n stdout: timedResult.stdout,\n stderr,\n timeSeconds: timedResult.status === 124 ? timeoutSeconds + 0.001 : timeSeconds,\n memoryBytes,\n };\n}\n\nasync function ensureTomcatStopped(catalinaHome: string, catalinaBaseDir: string): Promise<void> {\n if (!(await isTomcatRunning())) {\n return;\n }\n\n runCommand([path.join(catalinaHome, 'bin', 'catalina.sh'), 'stop'], undefined, 10, {\n ...process.env,\n CATALINA_BASE: catalinaBaseDir,\n CATALINA_HOME: catalinaHome,\n });\n await waitForTomcatState(false, 10_000);\n}\n\nasync function waitForTomcatState(expectedRunning: boolean, timeoutMs: number): Promise<boolean> {\n const startedAt = Date.now();\n while (Date.now() - startedAt < timeoutMs) {\n if ((await isTomcatRunning()) === expectedRunning) {\n return true;\n }\n await sleep(200);\n }\n return (await isTomcatRunning()) === expectedRunning;\n}\n\nasync function isTomcatRunning(): Promise<boolean> {\n return await new Promise<boolean>((resolve) => {\n const request = http.get(TOMCAT_BASE_URL, (response) => {\n response.resume();\n const status = response.statusCode ?? 0;\n resolve((status >= 200 && status < 300) || status === 404);\n });\n request.on('error', () => resolve(false));\n request.setTimeout(1000, () => request.destroy());\n });\n}\n\nfunction getCatalinaHome(): string {\n const catalinaHome = process.env.CATALINA_HOME;\n if (!catalinaHome) {\n throw new Error('CATALINA_HOME is not set');\n }\n\n validateCatalinaHome(catalinaHome);\n return path.resolve(catalinaHome);\n}\n\nfunction validateCatalinaHome(catalinaHome: string): void {\n if (\n !fs.existsSync(path.join(catalinaHome, 'bin', 'catalina.sh')) ||\n !fs.existsSync(path.join(catalinaHome, 'conf', 'server.xml'))\n ) {\n throw new Error('Invalid CATALINA_HOME path');\n }\n}\n\nfunction 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\nfunction truncateOutput(value: string): string {\n return value.slice(0, MAX_OUTPUT_LENGTH);\n}\n\nexport function buildTomcatUrl(urlPath: string): string {\n return `${TOMCAT_BASE_URL}/${APPLICATION_NAME}${urlPath.startsWith('/') ? urlPath : `/${urlPath}`}`;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exercode/problem-utils",
3
- "version": "3.2.2",
3
+ "version": "4.1.0",
4
4
  "description": ":100: A set of utilities for judging programs on Exercode (https://exercode.willbooster.com/).",
5
5
  "keywords": [
6
6
  "exercode",
@@ -8,7 +8,8 @@
8
8
  ],
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/WillBooster/exercode-problem-utils.git"
11
+ "url": "git+https://github.com/WillBooster/exercode-problem-utils.git",
12
+ "directory": "packages/problem-utils"
12
13
  },
13
14
  "license": "Apache-2.0",
14
15
  "author": "WillBooster Inc.",
@@ -37,55 +38,16 @@
37
38
  "dist/"
38
39
  ],
39
40
  "scripts": {
40
- "build": "build-ts lib --input src/index.ts src/presets/*.ts src/cli/*.ts",
41
- "cleanup": "bun wb lint --fix --format",
42
- "format": "bun wb lint --format",
43
- "lint": "bun wb lint",
44
- "lint-fix": "bun wb lint --fix",
45
- "prepare": "(bun lefthook install || true) && (test ! -f .lefthook/normalize-bun-lockfile.sh || bash .lefthook/normalize-bun-lockfile.sh)",
46
- "start": "bun src/index.ts",
47
- "test": "bun wb test",
48
- "test/ci": "bun wb test-on-ci",
49
- "test/ci-setup": "bun run build && bun install --cwd example && cd example && bun run puppeteer browsers install chrome",
50
- "typecheck": "bun wb typecheck",
51
- "verify": "bun wb verify",
52
- "verify-full": "bun wb verify --full"
41
+ "build": "build-ts lib --input src/index.ts src/presets/*.ts src/cli/*.ts"
53
42
  },
54
43
  "prettier": "@willbooster/prettier-config",
55
44
  "dependencies": {
56
- "@ai-sdk/amazon-bedrock": "5.0.67",
57
- "@ai-sdk/google": "4.0.57",
58
- "@ai-sdk/openai": "4.0.51",
59
- "@ai-sdk/xai": "4.0.49",
60
- "ai": "7.0.84",
61
45
  "front-matter": "4.0.2",
62
46
  "yaml": "2.9.0",
63
47
  "zod": "4.5.2"
64
48
  },
65
49
  "devDependencies": {
66
- "@tsconfig/bun": "1.0.11",
67
- "@tsconfig/node-lts": "24.0.1",
68
- "@tsconfig/node-ts": "23.6.4",
69
- "@tsconfig/node24": "24.0.5",
70
- "@types/bun": "1.4.0",
71
- "@types/node": "26.4.0",
72
- "@willbooster/oxfmt-config": "1.2.2",
73
- "@willbooster/oxlint-config": "1.4.8",
74
- "@willbooster/prettier-config": "10.4.0",
75
- "@willbooster/wb": "22.1.4",
76
- "build-ts": "21.0.12",
77
- "conventional-changelog-conventionalcommits": "10.4.0",
78
- "lefthook": "2.1.12",
79
- "oxfmt": "0.65.0",
80
- "oxlint": "1.80.0",
81
- "oxlint-tsgolint": "7.0.2001",
82
- "prettier": "3.9.6",
83
- "prettier-plugin-java": "2.10.3",
84
- "puppeteer": "25.9.0",
85
- "semantic-release": "25.0.9",
86
- "sort-package-json": "4.0.0",
87
- "typescript": "7.0.2",
88
- "vitest": "4.1.11"
50
+ "build-ts": "21.0.12"
89
51
  },
90
52
  "engines": {
91
53
  "node": ">=24"
@@ -1,2 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/printTestCaseResult.cjs"),n=require("../helpers/parseArgs.cjs"),r=require("../helpers/readTestCases.cjs"),i=require("../types/decisionCode.cjs");let a=require("node:fs");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);let s=require("zod"),c=require("@ai-sdk/amazon-bedrock"),l=require("@ai-sdk/google"),u=require("@ai-sdk/openai"),d=require("@ai-sdk/xai"),f=require("ai");const p={bedrock:c.bedrock,google:l.google,openai:u.openai,xai:d.xai},m=s.z.object({model:s.z.string().min(1)});async function h(e,s){let c=n.parseArgs(process.argv);if(!c.cwd)throw Error(`cwd argument required`);let l=m.parse(c.params),u=await r.readTestCases(o.default.join(e,`test_cases`)),d=await a.default.promises.readFile(o.default.join(c.cwd,`prompt.txt`),`utf8`);for(let e of u){let n=Date.now();try{let{text:r}=await(0,f.generateText)({model:_(l.model),...g(s.buildPrompt?.({prompt:d,testCase:e})??d.replaceAll(`{input}`,e.input??``))}),a=Date.now(),o={testCaseId:e.id,decisionCode:i.DecisionCode.ACCEPTED,stdin:e.input,stdout:r,timeSeconds:(a-n)/1e3,...await s.test({testCase:e,result:{output:r}})};if(t.printTestCaseResult(o),o.decisionCode!==i.DecisionCode.ACCEPTED)break}catch(r){let a=Date.now();t.printTestCaseResult({testCaseId:e.id,decisionCode:i.DecisionCode.RUNTIME_ERROR,stdin:e.input,stderr:r instanceof Error?r.message:String(r),timeSeconds:(a-n)/1e3});break}}}function g(e){return typeof e==`string`?{prompt:e}:{messages:e,allowSystemInMessages:!0}}function _(e){let[t,n]=e.split(`/`);if(!t||!n)throw Error(`bad model: ${e}`);let r=p[t]?.(n);if(!r)throw Error(`model not found: ${e}`);return r}exports.llmJudgePreset=h;
2
- //# sourceMappingURL=llm.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"llm.cjs","names":["z","parseArgs","readTestCases","path","fs","generateText","DecisionCode"],"sources":["../../src/presets/llm.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { bedrock } from '@ai-sdk/amazon-bedrock';\nimport { google } from '@ai-sdk/google';\nimport { openai } from '@ai-sdk/openai';\nimport { xai } from '@ai-sdk/xai';\nimport type { LanguageModel, ModelMessage } from 'ai';\nimport { generateText } from 'ai';\nimport { z } from 'zod';\n\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst PROMPT_FILENAME = 'prompt.txt';\n\nconst providerByName: Record<string, typeof bedrock | typeof google | typeof openai | typeof xai> = {\n // requires `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`\n bedrock,\n // requires `GOOGLE_GENERATIVE_AI_API_KEY`\n google,\n // requires `OPENAI_API_KEY`\n openai,\n // requires `XAI_API_KEY`\n xai,\n} as const;\n\nconst judgeParamsSchema = z.object({\n model: z.string().min(1),\n});\n\n/** A `test_cases/` entry; the preset uses `input` for the prompt and hands everything to `test`. */\ninterface LlmTestCase {\n id: string;\n input?: string;\n output?: string;\n /** Present when `<id>.fin/` exists; the preset does not copy it anywhere (there is no program run). */\n fileInputPath?: string;\n /** Present when `<id>.fout/` exists, for `test` to read. */\n fileOutputPath?: string;\n}\n\ninterface LlmJudgePresetOptions {\n buildPrompt?: (context: { prompt: string; testCase: LlmTestCase }) => string | ModelMessage[];\n test: (context: {\n testCase: LlmTestCase;\n result: { output: string };\n }) => Partial<TestCaseResult> | Promise<Partial<TestCaseResult>>;\n}\n\n/**\n * A preset judge function for running and testing a user prompt in LLM.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { llmJudgePreset } from '@exercode/problem-utils/presets/llm';\n * import { DecisionCode } from '@exercode/problem-utils';\n *\n * await llmJudgePreset(import.meta.dirname, {\n * test: (context) {\n * return { decisionCode: context.result.output ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER };\n * }\n * });\n * ```\n *\n * Run with the required parameters:\n * ```bash\n * bun judge.ts model_answers/java '{ \"model\": \"gemini-2.5-flash-lite\" }'\n * ```\n */\nexport async function llmJudgePreset(problemDir: string, options: LlmJudgePresetOptions): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n\n const prompt = await fs.promises.readFile(path.join(args.cwd, PROMPT_FILENAME), 'utf8');\n\n for (const testCase of testCases) {\n const startTimeMilliseconds = Date.now();\n try {\n const { text } = await generateText({\n model: toLanguageModel(params.model),\n ...toPromptOptions(\n options.buildPrompt?.({ prompt, testCase }) ?? prompt.replaceAll('{input}', testCase.input ?? '')\n ),\n });\n\n const stopTimeMilliseconds = Date.now();\n\n const testCaseResult = {\n testCaseId: testCase.id,\n decisionCode: DecisionCode.ACCEPTED,\n stdin: testCase.input,\n stdout: text,\n timeSeconds: (stopTimeMilliseconds - startTimeMilliseconds) / 1000,\n ...(await options.test({ testCase, result: { output: text } })),\n };\n\n printTestCaseResult(testCaseResult);\n\n if (testCaseResult.decisionCode !== DecisionCode.ACCEPTED) break;\n } catch (error) {\n const stopTimeMilliseconds = Date.now();\n\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: testCase.input,\n stderr: error instanceof Error ? error.message : String(error),\n timeSeconds: (stopTimeMilliseconds - startTimeMilliseconds) / 1000,\n });\n\n break;\n }\n }\n}\n\n/** `ai` rejects system messages inside `messages` unless told that a `buildPrompt` may place them there. */\nfunction toPromptOptions(\n built: string | ModelMessage[]\n): { prompt: string } | { messages: ModelMessage[]; allowSystemInMessages: true } {\n return typeof built === 'string' ? { prompt: built } : { messages: built, allowSystemInMessages: true };\n}\n\nfunction toLanguageModel(model: string): LanguageModel {\n const [providerId, modelId] = model.split('/');\n if (!providerId || !modelId) throw new Error(`bad model: ${model}`);\n const languageModel = providerByName[providerId]?.(modelId);\n if (!languageModel) throw new Error(`model not found: ${model}`);\n return languageModel;\n}\n"],"mappings":"4hBAiBA,MAEM,EAA8F,CAElG,QAAA,EAAA,QAEA,OAAA,EAAA,OAEA,OAAA,EAAA,OAEA,IAAA,EAAA,GACF,EAEM,EAAoBA,EAAAA,EAAE,OAAO,CACjC,MAAOA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CACzB,CAAC,EA0CD,eAAsB,EAAe,EAAoB,EAA+C,CACtG,IAAM,EAAOC,EAAAA,UAAU,QAAQ,IAAI,EACnC,GAAI,CAAC,EAAK,IAAK,MAAU,MAAM,uBAAuB,EACtD,IAAM,EAAS,EAAkB,MAAM,EAAK,MAAM,EAE5C,EAAY,MAAMC,EAAAA,cAAcC,EAAAA,QAAK,KAAK,EAAY,YAAY,CAAC,EAEnE,EAAS,MAAMC,EAAAA,QAAG,SAAS,SAASD,EAAAA,QAAK,KAAK,EAAK,IAAK,YAAe,EAAG,MAAM,EAEtF,IAAK,IAAM,KAAY,EAAW,CAChC,IAAM,EAAwB,KAAK,IAAI,EACvC,GAAI,CACF,GAAM,CAAE,QAAS,MAAA,EAAME,EAAAA,aAAAA,CAAa,CAClC,MAAO,EAAgB,EAAO,KAAK,EACnC,GAAG,EACD,EAAQ,cAAc,CAAE,SAAQ,UAAS,CAAC,GAAK,EAAO,WAAW,UAAW,EAAS,OAAS,EAAE,CAClG,CACF,CAAC,EAEK,EAAuB,KAAK,IAAI,EAEhC,EAAiB,CACrB,WAAY,EAAS,GACrB,aAAcC,EAAAA,aAAa,SAC3B,MAAO,EAAS,MAChB,OAAQ,EACR,aAAc,EAAuB,GAAyB,IAC9D,GAAI,MAAM,EAAQ,KAAK,CAAE,WAAU,OAAQ,CAAE,OAAQ,CAAK,CAAE,CAAC,CAC/D,EAIA,GAFA,EAAA,oBAAoB,CAAc,EAE9B,EAAe,eAAiBA,EAAAA,aAAa,SAAU,KAC7D,OAAS,EAAO,CACd,IAAM,EAAuB,KAAK,IAAI,EAEtC,EAAA,oBAAoB,CAClB,WAAY,EAAS,GACrB,aAAcA,EAAAA,aAAa,cAC3B,MAAO,EAAS,MAChB,OAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAC7D,aAAc,EAAuB,GAAyB,GAChE,CAAC,EAED,KACF,CACF,CACF,CAGA,SAAS,EACP,EACgF,CAChF,OAAO,OAAO,GAAU,SAAW,CAAE,OAAQ,CAAM,EAAI,CAAE,SAAU,EAAO,sBAAuB,EAAK,CACxG,CAEA,SAAS,EAAgB,EAA8B,CACrD,GAAM,CAAC,EAAY,GAAW,EAAM,MAAM,GAAG,EAC7C,GAAI,CAAC,GAAc,CAAC,EAAS,MAAU,MAAM,cAAc,GAAO,EAClE,IAAM,EAAgB,EAAe,EAAW,GAAG,CAAO,EAC1D,GAAI,CAAC,EAAe,MAAU,MAAM,oBAAoB,GAAO,EAC/D,OAAO,CACT"}
@@ -1,47 +0,0 @@
1
- import type { ModelMessage } from 'ai';
2
- import type { TestCaseResult } from '../types/testCaseResult.js';
3
- /** A `test_cases/` entry; the preset uses `input` for the prompt and hands everything to `test`. */
4
- interface LlmTestCase {
5
- id: string;
6
- input?: string;
7
- output?: string;
8
- /** Present when `<id>.fin/` exists; the preset does not copy it anywhere (there is no program run). */
9
- fileInputPath?: string;
10
- /** Present when `<id>.fout/` exists, for `test` to read. */
11
- fileOutputPath?: string;
12
- }
13
- interface LlmJudgePresetOptions {
14
- buildPrompt?: (context: {
15
- prompt: string;
16
- testCase: LlmTestCase;
17
- }) => string | ModelMessage[];
18
- test: (context: {
19
- testCase: LlmTestCase;
20
- result: {
21
- output: string;
22
- };
23
- }) => Partial<TestCaseResult> | Promise<Partial<TestCaseResult>>;
24
- }
25
- /**
26
- * A preset judge function for running and testing a user prompt in LLM.
27
- *
28
- * @example
29
- * Create `judge.ts`:
30
- * ```ts
31
- * import { llmJudgePreset } from '@exercode/problem-utils/presets/llm';
32
- * import { DecisionCode } from '@exercode/problem-utils';
33
- *
34
- * await llmJudgePreset(import.meta.dirname, {
35
- * test: (context) {
36
- * return { decisionCode: context.result.output ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER };
37
- * }
38
- * });
39
- * ```
40
- *
41
- * Run with the required parameters:
42
- * ```bash
43
- * bun judge.ts model_answers/java '{ "model": "gemini-2.5-flash-lite" }'
44
- * ```
45
- */
46
- export declare function llmJudgePreset(problemDir: string, options: LlmJudgePresetOptions): Promise<void>;
47
- export {};
@@ -1,2 +0,0 @@
1
- import{printTestCaseResult as e}from"../helpers/printTestCaseResult.js";import{parseArgs as t}from"../helpers/parseArgs.js";import{readTestCases as n}from"../helpers/readTestCases.js";import{DecisionCode as r}from"../types/decisionCode.js";import i from"node:fs";import a from"node:path";import{z as o}from"zod";import{bedrock as s}from"@ai-sdk/amazon-bedrock";import{google as c}from"@ai-sdk/google";import{openai as l}from"@ai-sdk/openai";import{xai as u}from"@ai-sdk/xai";import{generateText as d}from"ai";const f={bedrock:s,google:c,openai:l,xai:u},p=o.object({model:o.string().min(1)});async function m(o,s){let c=t(process.argv);if(!c.cwd)throw Error(`cwd argument required`);let l=p.parse(c.params),u=await n(a.join(o,`test_cases`)),f=await i.promises.readFile(a.join(c.cwd,`prompt.txt`),`utf8`);for(let t of u){let n=Date.now();try{let{text:i}=await d({model:g(l.model),...h(s.buildPrompt?.({prompt:f,testCase:t})??f.replaceAll(`{input}`,t.input??``))}),a=Date.now(),o={testCaseId:t.id,decisionCode:r.ACCEPTED,stdin:t.input,stdout:i,timeSeconds:(a-n)/1e3,...await s.test({testCase:t,result:{output:i}})};if(e(o),o.decisionCode!==r.ACCEPTED)break}catch(i){let a=Date.now();e({testCaseId:t.id,decisionCode:r.RUNTIME_ERROR,stdin:t.input,stderr:i instanceof Error?i.message:String(i),timeSeconds:(a-n)/1e3});break}}}function h(e){return typeof e==`string`?{prompt:e}:{messages:e,allowSystemInMessages:!0}}function g(e){let[t,n]=e.split(`/`);if(!t||!n)throw Error(`bad model: ${e}`);let r=f[t]?.(n);if(!r)throw Error(`model not found: ${e}`);return r}export{m as llmJudgePreset};
2
- //# sourceMappingURL=llm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"llm.js","names":[],"sources":["../../src/presets/llm.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { bedrock } from '@ai-sdk/amazon-bedrock';\nimport { google } from '@ai-sdk/google';\nimport { openai } from '@ai-sdk/openai';\nimport { xai } from '@ai-sdk/xai';\nimport type { LanguageModel, ModelMessage } from 'ai';\nimport { generateText } from 'ai';\nimport { z } from 'zod';\n\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { readTestCases } from '../helpers/readTestCases.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst PROMPT_FILENAME = 'prompt.txt';\n\nconst providerByName: Record<string, typeof bedrock | typeof google | typeof openai | typeof xai> = {\n // requires `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`\n bedrock,\n // requires `GOOGLE_GENERATIVE_AI_API_KEY`\n google,\n // requires `OPENAI_API_KEY`\n openai,\n // requires `XAI_API_KEY`\n xai,\n} as const;\n\nconst judgeParamsSchema = z.object({\n model: z.string().min(1),\n});\n\n/** A `test_cases/` entry; the preset uses `input` for the prompt and hands everything to `test`. */\ninterface LlmTestCase {\n id: string;\n input?: string;\n output?: string;\n /** Present when `<id>.fin/` exists; the preset does not copy it anywhere (there is no program run). */\n fileInputPath?: string;\n /** Present when `<id>.fout/` exists, for `test` to read. */\n fileOutputPath?: string;\n}\n\ninterface LlmJudgePresetOptions {\n buildPrompt?: (context: { prompt: string; testCase: LlmTestCase }) => string | ModelMessage[];\n test: (context: {\n testCase: LlmTestCase;\n result: { output: string };\n }) => Partial<TestCaseResult> | Promise<Partial<TestCaseResult>>;\n}\n\n/**\n * A preset judge function for running and testing a user prompt in LLM.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { llmJudgePreset } from '@exercode/problem-utils/presets/llm';\n * import { DecisionCode } from '@exercode/problem-utils';\n *\n * await llmJudgePreset(import.meta.dirname, {\n * test: (context) {\n * return { decisionCode: context.result.output ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER };\n * }\n * });\n * ```\n *\n * Run with the required parameters:\n * ```bash\n * bun judge.ts model_answers/java '{ \"model\": \"gemini-2.5-flash-lite\" }'\n * ```\n */\nexport async function llmJudgePreset(problemDir: string, options: LlmJudgePresetOptions): Promise<void> {\n const args = parseArgs(process.argv);\n if (!args.cwd) throw new Error('cwd argument required');\n const params = judgeParamsSchema.parse(args.params);\n\n const testCases = await readTestCases(path.join(problemDir, 'test_cases'));\n\n const prompt = await fs.promises.readFile(path.join(args.cwd, PROMPT_FILENAME), 'utf8');\n\n for (const testCase of testCases) {\n const startTimeMilliseconds = Date.now();\n try {\n const { text } = await generateText({\n model: toLanguageModel(params.model),\n ...toPromptOptions(\n options.buildPrompt?.({ prompt, testCase }) ?? prompt.replaceAll('{input}', testCase.input ?? '')\n ),\n });\n\n const stopTimeMilliseconds = Date.now();\n\n const testCaseResult = {\n testCaseId: testCase.id,\n decisionCode: DecisionCode.ACCEPTED,\n stdin: testCase.input,\n stdout: text,\n timeSeconds: (stopTimeMilliseconds - startTimeMilliseconds) / 1000,\n ...(await options.test({ testCase, result: { output: text } })),\n };\n\n printTestCaseResult(testCaseResult);\n\n if (testCaseResult.decisionCode !== DecisionCode.ACCEPTED) break;\n } catch (error) {\n const stopTimeMilliseconds = Date.now();\n\n printTestCaseResult({\n testCaseId: testCase.id,\n decisionCode: DecisionCode.RUNTIME_ERROR,\n stdin: testCase.input,\n stderr: error instanceof Error ? error.message : String(error),\n timeSeconds: (stopTimeMilliseconds - startTimeMilliseconds) / 1000,\n });\n\n break;\n }\n }\n}\n\n/** `ai` rejects system messages inside `messages` unless told that a `buildPrompt` may place them there. */\nfunction toPromptOptions(\n built: string | ModelMessage[]\n): { prompt: string } | { messages: ModelMessage[]; allowSystemInMessages: true } {\n return typeof built === 'string' ? { prompt: built } : { messages: built, allowSystemInMessages: true };\n}\n\nfunction toLanguageModel(model: string): LanguageModel {\n const [providerId, modelId] = model.split('/');\n if (!providerId || !modelId) throw new Error(`bad model: ${model}`);\n const languageModel = providerByName[providerId]?.(modelId);\n if (!languageModel) throw new Error(`model not found: ${model}`);\n return languageModel;\n}\n"],"mappings":"6fAiBA,MAEM,EAA8F,CAElG,UAEA,SAEA,SAEA,KACF,EAEM,EAAoB,EAAE,OAAO,CACjC,MAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CACzB,CAAC,EA0CD,eAAsB,EAAe,EAAoB,EAA+C,CACtG,IAAM,EAAO,EAAU,QAAQ,IAAI,EACnC,GAAI,CAAC,EAAK,IAAK,MAAU,MAAM,uBAAuB,EACtD,IAAM,EAAS,EAAkB,MAAM,EAAK,MAAM,EAE5C,EAAY,MAAM,EAAc,EAAK,KAAK,EAAY,YAAY,CAAC,EAEnE,EAAS,MAAM,EAAG,SAAS,SAAS,EAAK,KAAK,EAAK,IAAK,YAAe,EAAG,MAAM,EAEtF,IAAK,IAAM,KAAY,EAAW,CAChC,IAAM,EAAwB,KAAK,IAAI,EACvC,GAAI,CACF,GAAM,CAAE,QAAS,MAAM,EAAa,CAClC,MAAO,EAAgB,EAAO,KAAK,EACnC,GAAG,EACD,EAAQ,cAAc,CAAE,SAAQ,UAAS,CAAC,GAAK,EAAO,WAAW,UAAW,EAAS,OAAS,EAAE,CAClG,CACF,CAAC,EAEK,EAAuB,KAAK,IAAI,EAEhC,EAAiB,CACrB,WAAY,EAAS,GACrB,aAAc,EAAa,SAC3B,MAAO,EAAS,MAChB,OAAQ,EACR,aAAc,EAAuB,GAAyB,IAC9D,GAAI,MAAM,EAAQ,KAAK,CAAE,WAAU,OAAQ,CAAE,OAAQ,CAAK,CAAE,CAAC,CAC/D,EAIA,GAFA,EAAoB,CAAc,EAE9B,EAAe,eAAiB,EAAa,SAAU,KAC7D,OAAS,EAAO,CACd,IAAM,EAAuB,KAAK,IAAI,EAEtC,EAAoB,CAClB,WAAY,EAAS,GACrB,aAAc,EAAa,cAC3B,MAAO,EAAS,MAChB,OAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAC7D,aAAc,EAAuB,GAAyB,GAChE,CAAC,EAED,KACF,CACF,CACF,CAGA,SAAS,EACP,EACgF,CAChF,OAAO,OAAO,GAAU,SAAW,CAAE,OAAQ,CAAM,EAAI,CAAE,SAAU,EAAO,sBAAuB,EAAK,CACxG,CAEA,SAAS,EAAgB,EAA8B,CACrD,GAAM,CAAC,EAAY,GAAW,EAAM,MAAM,GAAG,EAC7C,GAAI,CAAC,GAAc,CAAC,EAAS,MAAU,MAAM,cAAc,GAAO,EAClE,IAAM,EAAgB,EAAe,EAAW,GAAG,CAAO,EAC1D,GAAI,CAAC,EAAe,MAAU,MAAM,oBAAoB,GAAO,EAC/D,OAAO,CACT"}