@exercode/problem-utils 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ "use strict";var e=class extends Error{reason;lineNumber;constructor(e,t){super(`${e.replaceAll(`_`,` `)} on line ${t}`),this.name=`CsvParseError`,this.reason=e,this.lineNumber=t}};function t(e){return n(e).map(e=>e.cells)}function n(t){let n=[],r=[],i=``,a=`start`,o=!1,s=1,c=1,l=0,u=t.startsWith(``)?t.slice(1):t,d=()=>{r.push(i),i=``,a=`start`};for(;l<u.length;){let t=u[l];if(a===`quoted`){if(t===`"`){if(u[l+1]===`"`){i+=`"`,l+=2;continue}a=`closed`}else(t===`
2
+ `||t===`\r`&&u[l+1]!==`
3
+ `)&&s++,i+=t;l++;continue}if(o=!0,t===`,`||t===`
4
+ `||t===`\r`)d(),t!==`,`&&(n.push({cells:r,lineNumber:c}),r=[],o=!1,t===`\r`&&u[l+1]===`
5
+ `&&l++,s++,c=s);else if(t===`"`){if(a!==`start`)throw new e(`unexpected_quote`,s);a=`quoted`}else if(a===`closed`)throw new e(`text_after_quoted_field`,s);else a=`unquoted`,i+=t;l++}if(a===`quoted`)throw new e(`unterminated_quote`,c);return o&&(d(),n.push({cells:r,lineNumber:c})),n}exports.CsvParseError=e,exports.parseCsv=t,exports.parseCsvRecords=n;
6
+ //# sourceMappingURL=parseCsv.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseCsv.cjs","names":[],"sources":["../../src/helpers/parseCsv.ts"],"sourcesContent":["export type CsvParseErrorReason = 'unterminated_quote' | 'unexpected_quote' | 'text_after_quoted_field';\n\nexport class CsvParseError extends Error {\n readonly reason: CsvParseErrorReason;\n /** 1-based physical line where the error was detected (the record's start line for an unterminated quote). */\n readonly lineNumber: number;\n\n constructor(reason: CsvParseErrorReason, lineNumber: number) {\n super(`${reason.replaceAll('_', ' ')} on line ${lineNumber}`);\n this.name = 'CsvParseError';\n this.reason = reason;\n this.lineNumber = lineNumber;\n }\n}\n\nexport interface CsvRecord {\n cells: string[];\n /** 1-based physical line on which the record starts (quoted fields may span several lines). */\n lineNumber: number;\n}\n\n/**\n * Parse RFC 4180 style CSV text (double-quoted fields, `\"\"` escapes, LF, CRLF, or CR line ends) into rows.\n * A trailing line end does not produce an extra row.\n *\n * @throws {CsvParseError} when a quoted field is not closed, or a quote appears anywhere but around a whole field.\n */\nexport function parseCsv(text: string): string[][] {\n return parseCsvRecords(text).map((record) => record.cells);\n}\n\n/** Like {@link parseCsv}, but also reports the physical line each record starts on. */\nexport function parseCsvRecords(text: string): CsvRecord[] {\n const records: CsvRecord[] = [];\n let cells: string[] = [];\n let field = '';\n let fieldState: 'start' | 'unquoted' | 'quoted' | 'closed' = 'start';\n let hasField = false;\n let lineNumber = 1;\n let recordLineNumber = 1;\n let index = 0;\n const content = text.startsWith('\\uFEFF') ? text.slice(1) : text;\n\n const endField = (): void => {\n cells.push(field);\n field = '';\n fieldState = 'start';\n };\n\n while (index < content.length) {\n const char = content[index] as string;\n if (fieldState === 'quoted') {\n if (char === '\"') {\n if (content[index + 1] === '\"') {\n field += '\"';\n index += 2;\n continue;\n }\n fieldState = 'closed';\n } else {\n if (char === '\\n' || (char === '\\r' && content[index + 1] !== '\\n')) lineNumber++;\n field += char;\n }\n index++;\n continue;\n }\n\n hasField = true;\n if (char === ',' || char === '\\n' || char === '\\r') {\n endField();\n if (char !== ',') {\n records.push({ cells, lineNumber: recordLineNumber });\n cells = [];\n hasField = false;\n if (char === '\\r' && content[index + 1] === '\\n') index++;\n lineNumber++;\n recordLineNumber = lineNumber;\n }\n } else if (char === '\"') {\n if (fieldState !== 'start') throw new CsvParseError('unexpected_quote', lineNumber);\n fieldState = 'quoted';\n } else if (fieldState === 'closed') {\n throw new CsvParseError('text_after_quoted_field', lineNumber);\n } else {\n fieldState = 'unquoted';\n field += char;\n }\n index++;\n }\n\n if (fieldState === 'quoted') throw new CsvParseError('unterminated_quote', recordLineNumber);\n if (hasField) {\n endField();\n records.push({ cells, lineNumber: recordLineNumber });\n }\n return records;\n}\n"],"mappings":"aAEA,IAAa,EAAb,cAAmC,KAAM,CACvC,OAEA,WAEA,YAAY,EAA6B,EAAoB,CAC3D,MAAM,GAAG,EAAO,WAAW,IAAK,GAAG,EAAE,WAAW,GAAY,EAC5D,KAAK,KAAO,gBACZ,KAAK,OAAS,EACd,KAAK,WAAa,CACpB,CACF,EAcA,SAAgB,EAAS,EAA0B,CACjD,OAAO,EAAgB,CAAI,CAAC,CAAC,IAAK,GAAW,EAAO,KAAK,CAC3D,CAGA,SAAgB,EAAgB,EAA2B,CACzD,IAAM,EAAuB,CAAC,EAC1B,EAAkB,CAAC,EACnB,EAAQ,GACR,EAAyD,QACzD,EAAW,GACX,EAAa,EACb,EAAmB,EACnB,EAAQ,EACN,EAAU,EAAK,WAAW,GAAQ,EAAI,EAAK,MAAM,CAAC,EAAI,EAEtD,MAAuB,CAC3B,EAAM,KAAK,CAAK,EAChB,EAAQ,GACR,EAAa,OACf,EAEA,KAAO,EAAQ,EAAQ,QAAQ,CAC7B,IAAM,EAAO,EAAQ,GACrB,GAAI,IAAe,SAAU,CAC3B,GAAI,IAAS,IAAK,CAChB,GAAI,EAAQ,EAAQ,KAAO,IAAK,CAC9B,GAAS,IACT,GAAS,EACT,QACF,CACA,EAAa,QACf,MACM,IAAS;GAAS,IAAS,MAAQ,EAAQ,EAAQ,KAAO;IAAO,IACrE,GAAS,EAEX,IACA,QACF,CAGA,GADA,EAAW,GACP,IAAS,KAAO,IAAS;GAAQ,IAAS,KAC5C,EAAS,EACL,IAAS,MACX,EAAQ,KAAK,CAAE,QAAO,WAAY,CAAiB,CAAC,EACpD,EAAQ,CAAC,EACT,EAAW,GACP,IAAS,MAAQ,EAAQ,EAAQ,KAAO;GAAM,IAClD,IACA,EAAmB,QAEhB,GAAI,IAAS,IAAK,CACvB,GAAI,IAAe,QAAS,MAAM,IAAI,EAAc,mBAAoB,CAAU,EAClF,EAAa,QACf,MAAO,GAAI,IAAe,SACxB,MAAM,IAAI,EAAc,0BAA2B,CAAU,MAE7D,GAAa,WACb,GAAS,EAEX,GACF,CAEA,GAAI,IAAe,SAAU,MAAM,IAAI,EAAc,qBAAsB,CAAgB,EAK3F,OAJI,IACF,EAAS,EACT,EAAQ,KAAK,CAAE,QAAO,WAAY,CAAiB,CAAC,GAE/C,CACT"}
@@ -0,0 +1,21 @@
1
+ export type CsvParseErrorReason = 'unterminated_quote' | 'unexpected_quote' | 'text_after_quoted_field';
2
+ export declare class CsvParseError extends Error {
3
+ readonly reason: CsvParseErrorReason;
4
+ /** 1-based physical line where the error was detected (the record's start line for an unterminated quote). */
5
+ readonly lineNumber: number;
6
+ constructor(reason: CsvParseErrorReason, lineNumber: number);
7
+ }
8
+ export interface CsvRecord {
9
+ cells: string[];
10
+ /** 1-based physical line on which the record starts (quoted fields may span several lines). */
11
+ lineNumber: number;
12
+ }
13
+ /**
14
+ * Parse RFC 4180 style CSV text (double-quoted fields, `""` escapes, LF, CRLF, or CR line ends) into rows.
15
+ * A trailing line end does not produce an extra row.
16
+ *
17
+ * @throws {CsvParseError} when a quoted field is not closed, or a quote appears anywhere but around a whole field.
18
+ */
19
+ export declare function parseCsv(text: string): string[][];
20
+ /** Like {@link parseCsv}, but also reports the physical line each record starts on. */
21
+ export declare function parseCsvRecords(text: string): CsvRecord[];
@@ -0,0 +1,6 @@
1
+ var e=class extends Error{reason;lineNumber;constructor(e,t){super(`${e.replaceAll(`_`,` `)} on line ${t}`),this.name=`CsvParseError`,this.reason=e,this.lineNumber=t}};function t(e){return n(e).map(e=>e.cells)}function n(t){let n=[],r=[],i=``,a=`start`,o=!1,s=1,c=1,l=0,u=t.startsWith(``)?t.slice(1):t,d=()=>{r.push(i),i=``,a=`start`};for(;l<u.length;){let t=u[l];if(a===`quoted`){if(t===`"`){if(u[l+1]===`"`){i+=`"`,l+=2;continue}a=`closed`}else(t===`
2
+ `||t===`\r`&&u[l+1]!==`
3
+ `)&&s++,i+=t;l++;continue}if(o=!0,t===`,`||t===`
4
+ `||t===`\r`)d(),t!==`,`&&(n.push({cells:r,lineNumber:c}),r=[],o=!1,t===`\r`&&u[l+1]===`
5
+ `&&l++,s++,c=s);else if(t===`"`){if(a!==`start`)throw new e(`unexpected_quote`,s);a=`quoted`}else if(a===`closed`)throw new e(`text_after_quoted_field`,s);else a=`unquoted`,i+=t;l++}if(a===`quoted`)throw new e(`unterminated_quote`,c);return o&&(d(),n.push({cells:r,lineNumber:c})),n}export{e as CsvParseError,t as parseCsv,n as parseCsvRecords};
6
+ //# sourceMappingURL=parseCsv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseCsv.js","names":[],"sources":["../../src/helpers/parseCsv.ts"],"sourcesContent":["export type CsvParseErrorReason = 'unterminated_quote' | 'unexpected_quote' | 'text_after_quoted_field';\n\nexport class CsvParseError extends Error {\n readonly reason: CsvParseErrorReason;\n /** 1-based physical line where the error was detected (the record's start line for an unterminated quote). */\n readonly lineNumber: number;\n\n constructor(reason: CsvParseErrorReason, lineNumber: number) {\n super(`${reason.replaceAll('_', ' ')} on line ${lineNumber}`);\n this.name = 'CsvParseError';\n this.reason = reason;\n this.lineNumber = lineNumber;\n }\n}\n\nexport interface CsvRecord {\n cells: string[];\n /** 1-based physical line on which the record starts (quoted fields may span several lines). */\n lineNumber: number;\n}\n\n/**\n * Parse RFC 4180 style CSV text (double-quoted fields, `\"\"` escapes, LF, CRLF, or CR line ends) into rows.\n * A trailing line end does not produce an extra row.\n *\n * @throws {CsvParseError} when a quoted field is not closed, or a quote appears anywhere but around a whole field.\n */\nexport function parseCsv(text: string): string[][] {\n return parseCsvRecords(text).map((record) => record.cells);\n}\n\n/** Like {@link parseCsv}, but also reports the physical line each record starts on. */\nexport function parseCsvRecords(text: string): CsvRecord[] {\n const records: CsvRecord[] = [];\n let cells: string[] = [];\n let field = '';\n let fieldState: 'start' | 'unquoted' | 'quoted' | 'closed' = 'start';\n let hasField = false;\n let lineNumber = 1;\n let recordLineNumber = 1;\n let index = 0;\n const content = text.startsWith('\\uFEFF') ? text.slice(1) : text;\n\n const endField = (): void => {\n cells.push(field);\n field = '';\n fieldState = 'start';\n };\n\n while (index < content.length) {\n const char = content[index] as string;\n if (fieldState === 'quoted') {\n if (char === '\"') {\n if (content[index + 1] === '\"') {\n field += '\"';\n index += 2;\n continue;\n }\n fieldState = 'closed';\n } else {\n if (char === '\\n' || (char === '\\r' && content[index + 1] !== '\\n')) lineNumber++;\n field += char;\n }\n index++;\n continue;\n }\n\n hasField = true;\n if (char === ',' || char === '\\n' || char === '\\r') {\n endField();\n if (char !== ',') {\n records.push({ cells, lineNumber: recordLineNumber });\n cells = [];\n hasField = false;\n if (char === '\\r' && content[index + 1] === '\\n') index++;\n lineNumber++;\n recordLineNumber = lineNumber;\n }\n } else if (char === '\"') {\n if (fieldState !== 'start') throw new CsvParseError('unexpected_quote', lineNumber);\n fieldState = 'quoted';\n } else if (fieldState === 'closed') {\n throw new CsvParseError('text_after_quoted_field', lineNumber);\n } else {\n fieldState = 'unquoted';\n field += char;\n }\n index++;\n }\n\n if (fieldState === 'quoted') throw new CsvParseError('unterminated_quote', recordLineNumber);\n if (hasField) {\n endField();\n records.push({ cells, lineNumber: recordLineNumber });\n }\n return records;\n}\n"],"mappings":"AAEA,IAAa,EAAb,cAAmC,KAAM,CACvC,OAEA,WAEA,YAAY,EAA6B,EAAoB,CAC3D,MAAM,GAAG,EAAO,WAAW,IAAK,GAAG,EAAE,WAAW,GAAY,EAC5D,KAAK,KAAO,gBACZ,KAAK,OAAS,EACd,KAAK,WAAa,CACpB,CACF,EAcA,SAAgB,EAAS,EAA0B,CACjD,OAAO,EAAgB,CAAI,CAAC,CAAC,IAAK,GAAW,EAAO,KAAK,CAC3D,CAGA,SAAgB,EAAgB,EAA2B,CACzD,IAAM,EAAuB,CAAC,EAC1B,EAAkB,CAAC,EACnB,EAAQ,GACR,EAAyD,QACzD,EAAW,GACX,EAAa,EACb,EAAmB,EACnB,EAAQ,EACN,EAAU,EAAK,WAAW,GAAQ,EAAI,EAAK,MAAM,CAAC,EAAI,EAEtD,MAAuB,CAC3B,EAAM,KAAK,CAAK,EAChB,EAAQ,GACR,EAAa,OACf,EAEA,KAAO,EAAQ,EAAQ,QAAQ,CAC7B,IAAM,EAAO,EAAQ,GACrB,GAAI,IAAe,SAAU,CAC3B,GAAI,IAAS,IAAK,CAChB,GAAI,EAAQ,EAAQ,KAAO,IAAK,CAC9B,GAAS,IACT,GAAS,EACT,QACF,CACA,EAAa,QACf,MACM,IAAS;GAAS,IAAS,MAAQ,EAAQ,EAAQ,KAAO;IAAO,IACrE,GAAS,EAEX,IACA,QACF,CAGA,GADA,EAAW,GACP,IAAS,KAAO,IAAS;GAAQ,IAAS,KAC5C,EAAS,EACL,IAAS,MACX,EAAQ,KAAK,CAAE,QAAO,WAAY,CAAiB,CAAC,EACpD,EAAQ,CAAC,EACT,EAAW,GACP,IAAS,MAAQ,EAAQ,EAAQ,KAAO;GAAM,IAClD,IACA,EAAmB,QAEhB,GAAI,IAAS,IAAK,CACvB,GAAI,IAAe,QAAS,MAAM,IAAI,EAAc,mBAAoB,CAAU,EAClF,EAAa,QACf,MAAO,GAAI,IAAe,SACxB,MAAM,IAAI,EAAc,0BAA2B,CAAU,MAE7D,GAAa,WACb,GAAS,EAEX,GACF,CAEA,GAAI,IAAe,SAAU,MAAM,IAAI,EAAc,qBAAsB,CAAgB,EAK3F,OAJI,IACF,EAAS,EACT,EAAQ,KAAK,CAAE,QAAO,WAAY,CAAiB,CAAC,GAE/C,CACT"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/defaultStdioHarness.cjs"),t=require("./helpers/parseArgs.cjs"),n=require("./types/testCaseResult.cjs"),r=require("./helpers/printTestCaseResult.cjs"),i=require("./helpers/removeCommentsInSourceCode.cjs"),a=require("./helpers/sandboxUser.cjs"),o=require("./helpers/safeFs.cjs"),s=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),c=require("./helpers/runPythonInTemporaryUvProject.cjs"),l=require("./helpers/sourceCodeGrammars.cjs"),u=require("./helpers/startHttpServer.cjs"),d=require("./types/decisionCode.cjs");exports.DecisionCode=d.DecisionCode,exports.HARNESS_FILE_PRESETS=e.HARNESS_FILE_PRESETS,exports.SANDBOX_ENV_PREFIX=a.SANDBOX_ENV_PREFIX,exports.SANDBOX_USER_ENV_NAME=a.SANDBOX_USER_ENV_NAME,exports.SANDBOX_WATCHDOG_GRACE_SECONDS=a.SANDBOX_WATCHDOG_GRACE_SECONDS,exports.TEST_CASE_RESULT_PREFIX=n.TEST_CASE_RESULT_PREFIX,exports.TIMEOUT_COMMAND=a.TIMEOUT_COMMAND,exports.copyPackageManagerProjectFiles=s.copyPackageManagerProjectFiles,exports.copyWithoutFollowingSymlinks=o.copyWithoutFollowingSymlinks,exports.createDirectoryWithoutFollowingSymlinks=o.createDirectoryWithoutFollowingSymlinks,exports.encodeFileForTestCaseResult=r.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=e.findDefaultStdioHarnessFiles,exports.forceRemove=a.forceRemove,exports.getSandboxUserEnvOverrides=a.getSandboxUserEnvOverrides,exports.getTrustedHelperEnv=a.getTrustedHelperEnv,exports.isContainedPath=o.isContainedPath,exports.isDefaultStdioHarnessSource=e.isDefaultStdioHarnessSource,exports.isSandboxWrappedCommand=a.isSandboxWrappedCommand,exports.killSandboxUserProcesses=a.killSandboxUserProcesses,exports.languageIdToSourceCodeGrammar=l.languageIdToSourceCodeGrammar,exports.makeAccessibleToSandboxUser=a.makeAccessibleToSandboxUser,exports.parseArgs=t.parseArgs,exports.prependInsideSandboxWrapper=a.prependInsideSandboxWrapper,exports.printTestCaseResult=r.printTestCaseResult,exports.relaxPermissionsAsSandboxUser=a.relaxPermissionsAsSandboxUser,exports.removeCommentsAndStringsInSourceCode=i.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=i.removeCommentsInSourceCode,exports.runCommandInTemporaryPackageManagerProject=s.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=c.runPythonInTemporaryUvProject,exports.sandboxUserName=a.sandboxUserName,exports.startHttpServer=u.startHttpServer,exports.startSandboxTimeoutWatchdog=a.startSandboxTimeoutWatchdog,exports.testCaseResultSchema=n.testCaseResultSchema,exports.wrapCommandWithSandboxUser=a.wrapCommandWithSandboxUser,exports.writeFileWithoutFollowingSymlinks=o.writeFileWithoutFollowingSymlinks;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/defaultStdioHarness.cjs"),t=require("./helpers/parseArgs.cjs"),n=require("./helpers/parseCsv.cjs"),r=require("./types/testCaseResult.cjs"),i=require("./helpers/printTestCaseResult.cjs"),a=require("./helpers/removeCommentsInSourceCode.cjs"),o=require("./helpers/sandboxUser.cjs"),s=require("./helpers/safeFs.cjs"),c=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),l=require("./helpers/runPythonInTemporaryUvProject.cjs"),u=require("./helpers/sourceCodeGrammars.cjs"),d=require("./helpers/startHttpServer.cjs"),f=require("./types/decisionCode.cjs");exports.CsvParseError=n.CsvParseError,exports.DecisionCode=f.DecisionCode,exports.HARNESS_FILE_PRESETS=e.HARNESS_FILE_PRESETS,exports.SANDBOX_ENV_PREFIX=o.SANDBOX_ENV_PREFIX,exports.SANDBOX_USER_ENV_NAME=o.SANDBOX_USER_ENV_NAME,exports.SANDBOX_WATCHDOG_GRACE_SECONDS=o.SANDBOX_WATCHDOG_GRACE_SECONDS,exports.TEST_CASE_RESULT_PREFIX=r.TEST_CASE_RESULT_PREFIX,exports.TIMEOUT_COMMAND=o.TIMEOUT_COMMAND,exports.copyPackageManagerProjectFiles=c.copyPackageManagerProjectFiles,exports.copyWithoutFollowingSymlinks=s.copyWithoutFollowingSymlinks,exports.createDirectoryWithoutFollowingSymlinks=s.createDirectoryWithoutFollowingSymlinks,exports.encodeFileForTestCaseResult=i.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=e.findDefaultStdioHarnessFiles,exports.forceRemove=o.forceRemove,exports.getSandboxUserEnvOverrides=o.getSandboxUserEnvOverrides,exports.getTrustedHelperEnv=o.getTrustedHelperEnv,exports.isContainedPath=s.isContainedPath,exports.isDefaultStdioHarnessSource=e.isDefaultStdioHarnessSource,exports.isSandboxWrappedCommand=o.isSandboxWrappedCommand,exports.killSandboxUserProcesses=o.killSandboxUserProcesses,exports.languageIdToSourceCodeGrammar=u.languageIdToSourceCodeGrammar,exports.makeAccessibleToSandboxUser=o.makeAccessibleToSandboxUser,exports.parseArgs=t.parseArgs,exports.parseCsv=n.parseCsv,exports.parseCsvRecords=n.parseCsvRecords,exports.prependInsideSandboxWrapper=o.prependInsideSandboxWrapper,exports.printTestCaseResult=i.printTestCaseResult,exports.relaxPermissionsAsSandboxUser=o.relaxPermissionsAsSandboxUser,exports.removeCommentsAndStringsInSourceCode=a.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=a.removeCommentsInSourceCode,exports.runCommandInTemporaryPackageManagerProject=c.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=l.runPythonInTemporaryUvProject,exports.sandboxUserName=o.sandboxUserName,exports.startHttpServer=d.startHttpServer,exports.startSandboxTimeoutWatchdog=o.startSandboxTimeoutWatchdog,exports.testCaseResultSchema=r.testCaseResultSchema,exports.wrapCommandWithSandboxUser=o.wrapCommandWithSandboxUser,exports.writeFileWithoutFollowingSymlinks=s.writeFileWithoutFollowingSymlinks;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './helpers/defaultStdioHarness.js';
2
2
  export * from './helpers/parseArgs.js';
3
+ export * from './helpers/parseCsv.js';
3
4
  export * from './helpers/printTestCaseResult.js';
4
5
  export * from './helpers/removeCommentsInSourceCode.js';
5
6
  export * from './helpers/runCommandInTemporaryPackageManagerProject.js';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{HARNESS_FILE_PRESETS as e,findDefaultStdioHarnessFiles as t,isDefaultStdioHarnessSource as n}from"./helpers/defaultStdioHarness.js";import{parseArgs as r}from"./helpers/parseArgs.js";import{TEST_CASE_RESULT_PREFIX as i,testCaseResultSchema as a}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as o,printTestCaseResult as s}from"./helpers/printTestCaseResult.js";import{removeCommentsAndStringsInSourceCode as c,removeCommentsInSourceCode as l}from"./helpers/removeCommentsInSourceCode.js";import{SANDBOX_ENV_PREFIX as u,SANDBOX_USER_ENV_NAME as d,SANDBOX_WATCHDOG_GRACE_SECONDS as f,TIMEOUT_COMMAND as p,forceRemove as m,getSandboxUserEnvOverrides as h,getTrustedHelperEnv as g,isSandboxWrappedCommand as _,killSandboxUserProcesses as v,makeAccessibleToSandboxUser as y,prependInsideSandboxWrapper as b,relaxPermissionsAsSandboxUser as x,sandboxUserName as S,startSandboxTimeoutWatchdog as C,wrapCommandWithSandboxUser as w}from"./helpers/sandboxUser.js";import{copyWithoutFollowingSymlinks as T,createDirectoryWithoutFollowingSymlinks as E,isContainedPath as D,writeFileWithoutFollowingSymlinks as O}from"./helpers/safeFs.js";import{copyPackageManagerProjectFiles as k,runCommandInTemporaryPackageManagerProject as A}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as j}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as M}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as N}from"./helpers/startHttpServer.js";import{DecisionCode as P}from"./types/decisionCode.js";export{P as DecisionCode,e as HARNESS_FILE_PRESETS,u as SANDBOX_ENV_PREFIX,d as SANDBOX_USER_ENV_NAME,f as SANDBOX_WATCHDOG_GRACE_SECONDS,i as TEST_CASE_RESULT_PREFIX,p as TIMEOUT_COMMAND,k as copyPackageManagerProjectFiles,T as copyWithoutFollowingSymlinks,E as createDirectoryWithoutFollowingSymlinks,o as encodeFileForTestCaseResult,t as findDefaultStdioHarnessFiles,m as forceRemove,h as getSandboxUserEnvOverrides,g as getTrustedHelperEnv,D as isContainedPath,n as isDefaultStdioHarnessSource,_ as isSandboxWrappedCommand,v as killSandboxUserProcesses,M as languageIdToSourceCodeGrammar,y as makeAccessibleToSandboxUser,r as parseArgs,b as prependInsideSandboxWrapper,s as printTestCaseResult,x as relaxPermissionsAsSandboxUser,c as removeCommentsAndStringsInSourceCode,l as removeCommentsInSourceCode,A as runCommandInTemporaryPackageManagerProject,j as runPythonInTemporaryUvProject,S as sandboxUserName,N as startHttpServer,C as startSandboxTimeoutWatchdog,a as testCaseResultSchema,w as wrapCommandWithSandboxUser,O as writeFileWithoutFollowingSymlinks};
1
+ import{HARNESS_FILE_PRESETS as e,findDefaultStdioHarnessFiles as t,isDefaultStdioHarnessSource as n}from"./helpers/defaultStdioHarness.js";import{parseArgs as r}from"./helpers/parseArgs.js";import{CsvParseError as i,parseCsv as a,parseCsvRecords as o}from"./helpers/parseCsv.js";import{TEST_CASE_RESULT_PREFIX as s,testCaseResultSchema as c}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as l,printTestCaseResult as u}from"./helpers/printTestCaseResult.js";import{removeCommentsAndStringsInSourceCode as d,removeCommentsInSourceCode as f}from"./helpers/removeCommentsInSourceCode.js";import{SANDBOX_ENV_PREFIX as p,SANDBOX_USER_ENV_NAME as m,SANDBOX_WATCHDOG_GRACE_SECONDS as h,TIMEOUT_COMMAND as g,forceRemove as _,getSandboxUserEnvOverrides as v,getTrustedHelperEnv as y,isSandboxWrappedCommand as b,killSandboxUserProcesses as x,makeAccessibleToSandboxUser as S,prependInsideSandboxWrapper as C,relaxPermissionsAsSandboxUser as w,sandboxUserName as T,startSandboxTimeoutWatchdog as E,wrapCommandWithSandboxUser as D}from"./helpers/sandboxUser.js";import{copyWithoutFollowingSymlinks as O,createDirectoryWithoutFollowingSymlinks as k,isContainedPath as A,writeFileWithoutFollowingSymlinks as j}from"./helpers/safeFs.js";import{copyPackageManagerProjectFiles as M,runCommandInTemporaryPackageManagerProject as N}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as P}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as F}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as I}from"./helpers/startHttpServer.js";import{DecisionCode as L}from"./types/decisionCode.js";export{i as CsvParseError,L as DecisionCode,e as HARNESS_FILE_PRESETS,p as SANDBOX_ENV_PREFIX,m as SANDBOX_USER_ENV_NAME,h as SANDBOX_WATCHDOG_GRACE_SECONDS,s as TEST_CASE_RESULT_PREFIX,g as TIMEOUT_COMMAND,M as copyPackageManagerProjectFiles,O as copyWithoutFollowingSymlinks,k as createDirectoryWithoutFollowingSymlinks,l as encodeFileForTestCaseResult,t as findDefaultStdioHarnessFiles,_ as forceRemove,v as getSandboxUserEnvOverrides,y as getTrustedHelperEnv,A as isContainedPath,n as isDefaultStdioHarnessSource,b as isSandboxWrappedCommand,x as killSandboxUserProcesses,F as languageIdToSourceCodeGrammar,S as makeAccessibleToSandboxUser,r as parseArgs,a as parseCsv,o as parseCsvRecords,C as prependInsideSandboxWrapper,u as printTestCaseResult,w as relaxPermissionsAsSandboxUser,d as removeCommentsAndStringsInSourceCode,f as removeCommentsInSourceCode,N as runCommandInTemporaryPackageManagerProject,P as runPythonInTemporaryUvProject,T as sandboxUserName,I as startHttpServer,E as startSandboxTimeoutWatchdog,c as testCaseResultSchema,D as wrapCommandWithSandboxUser,j as writeFileWithoutFollowingSymlinks};
@@ -0,0 +1,11 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../helpers/parseArgs.cjs"),n=require("../helpers/parseCsv.cjs"),r=require("../helpers/printTestCaseResult.cjs"),i=require("../types/decisionCode.cjs"),a=require("../helpers/printDebugBanner.cjs"),o=require("../helpers/checkProblemDirIsolation.cjs"),s=require("../helpers/judgeByStaticAnalysis.cjs"),c=require("../helpers/readOutputFiles.cjs"),l=require("../helpers/readProblemMarkdownFrontMatter.cjs"),u=require("../helpers/resolveCwds.cjs");let d=require("node:fs/promises");d=e.__toESM(d,1);let f=require("node:path");f=e.__toESM(f,1);const p={unterminated_quote:'引用符(`"`)が閉じられていません',unexpected_quote:'引用符(`"`)は値全体を囲む位置にしか書けません',text_after_quoted_field:`閉じ引用符の後に区切り文字以外の文字があります`},m={rmsle:{label:`RMSLE`,isHigherBetter:!1,validatePrediction:e=>D(e)??(Number(e)<0?`負の値です`:void 0),compute:(e,t)=>Math.sqrt(O(e.map((e,n)=>(Math.log1p(Number(e))-Math.log1p(Number(t[n])))**2)))},rmse:{label:`RMSE`,isHigherBetter:!1,validatePrediction:D,compute:(e,t)=>Math.sqrt(O(e.map((e,n)=>(Number(e)-Number(t[n]))**2)))},mae:{label:`MAE`,isHigherBetter:!1,validatePrediction:D,compute:(e,t)=>O(e.map((e,n)=>Math.abs(Number(e)-Number(t[n]))))},accuracy:{label:`Accuracy`,isHigherBetter:!0,validatePrediction:e=>e.trim()===``?`空です`:void 0,compute:(e,t)=>O(e.map((e,n)=>+(e.trim()===t[n]?.trim())))}};async function h(e,n){let i=t.parseArgs(process.argv),{cwds:s,isDebugMode:c}=await u.resolveCwds(e,i.cwd);if(c){let t=s.find(e=>e.expectedResult===`accepted`);if(t){if(!(await o.checkProblemDirIsolation(e,t,i.params)).passed){process.exitCode=1;return}}else a.printDebugBanner([`[DEBUG MODE] isolated problem directory check skipped`,``,`No accepted model answer is available for checking that the copied judge still accepts a valid submission.`])}let l=await x(f.default.join(e,n.answerFilePath),n);for(let t of s){c&&u.printDebugCwdBanner(e,t);let i=await g(e,t.cwd,l,n);r.printTestCaseResult({testCaseId:`evaluation`,...i}),c&&!E(t,i)&&(process.exitCode=1,u.printDebugExpectationFailureBanner(e,t))}}async function g(e,t,n,r){let a=await s.judgeByStaticAnalysis(t,await l.readProblemMarkdownFrontMatter(e));if(a)return a;let o=r.submissionFilePath??`submission.csv`,u;try{let e=f.default.join(t,o);if(!await c.isSafeSubmissionOutputPath(t,e))throw Error(`unsafe path`);u=await d.default.readFile(e,`utf8`)}catch{return{decisionCode:i.DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`\`${o}\`が見つかりません。予測結果のCSVファイルを\`${o}\`という名前で提出してください。`}}let p=b(u,o,n,r);if(typeof p==`string`)return{decisionCode:i.DecisionCode.WRONG_ANSWER,feedbackMarkdown:p};let m=[...n.values()],h=r.metric.compute([...n.keys()].map(e=>p.get(e)),m);if(!Number.isFinite(h))return{decisionCode:i.DecisionCode.WRONG_ANSWER,feedbackMarkdown:`予測値が極端なため、${r.metric.label}を計算できませんでした。予測値を見直してください。`};let g=r.acceptableScore===void 0||(r.metric.isHigherBetter?h>=r.acceptableScore:h<=r.acceptableScore);return{decisionCode:g?i.DecisionCode.ACCEPTED:i.DecisionCode.WRONG_ANSWER,score:h,scoreLabel:r.metric.label,feedbackMarkdown:_(h,m.length,g,r)}}function _(e,t,n,r){let{label:i,isHigherBetter:a}=r.metric,o=v(e,r.acceptableScore),s=r.acceptableScore===void 0?``:`\n合格基準: ${i} ${a?`≧`:`≦`} ${y(r.acceptableScore,o)}(${n?`達成`:`未達成`})`;return`| 指標 | スコア |
2
+ | ---- | ------ |
3
+ | ${i} | ${y(e,o)} |
4
+
5
+ 評価件数: ${t.toLocaleString(`en-US`)}件${s}
6
+ `}function v(e,t){if(t===void 0||e===t)return 6;for(let n=6;n<17;n++)if(y(e,n)!==y(t,n))return n;return 17}function y(e,t){return Number(e.toPrecision(t)).toString()}function b(e,t,r,i){let a;try{a=n.parseCsvRecords(e)}catch(e){return`\`${t}\`をCSVとして読み込めませんでした。${e instanceof n.CsvParseError?`${e.lineNumber}行目付近: ${p[e.reason]}`:``}`}let o=S(a[0]?.cells??[],i);if(!o)return`\`${t}\`の1行目には\`${i.idColumn}\`列と\`${i.targetColumn}\`列のヘッダーが必要です。`;let s=new Map,c=[],l=0,u=e=>{l++,c.length<10&&c.push(e)};for(let{cells:e,lineNumber:t}of a.slice(1)){if(C(e))continue;let n=e[o.idIndex]?.trim()??``,a=e[o.targetIndex]??``;if(!r.has(n))u(`${t}行目: \`${i.idColumn}\`が\`${T(n)}\`の行は評価対象ではありません`);else if(s.has(n))u(`${t}行目: \`${i.idColumn}\`が\`${T(n)}\`の行が重複しています`);else{let e=i.metric.validatePrediction?.(a);e&&u(`${t}行目: \`${i.targetColumn}\`の値\`${T(a)}\`は${e}`),s.set(n,a)}}let d=[...r.keys()].filter(e=>!s.has(e));return d.length>0&&(l++,c.unshift(`\`${i.idColumn}\`が${w(d)}の行がありません(不足 ${d.length.toLocaleString(`en-US`)}件)`)),l>0?`\`${t}\`の内容に問題があります。
7
+
8
+ ${c.map(e=>`- ${e}`).join(`
9
+ `)}${l>c.length?`\n- ほか${(l-c.length).toLocaleString(`en-US`)}件`:``}
10
+ `:s}async function x(e,t){let r=n.parseCsv(await d.default.readFile(e,`utf8`)),i=S(r[0]??[],t);if(!i)throw Error(`answer file must have ${t.idColumn} and ${t.targetColumn} columns: ${e}`);let a=new Map;for(let n of r.slice(1)){if(C(n))continue;let r=n[i.idIndex]?.trim()??``,o=n[i.targetIndex]??``;if(!r)throw Error(`empty ${t.idColumn} in answer file: ${e}`);if(a.has(r))throw Error(`duplicate ${t.idColumn} in answer file: ${r}`);let s=t.metric.validatePrediction?.(o);if(s)throw Error(`invalid ${t.targetColumn} in answer file for ${r}: ${s}`);a.set(r,o)}if(a.size===0)throw Error(`answer file has no rows: ${e}`);return a}function S(e,t){let n=e.map(e=>e.trim()),r=n.indexOf(t.idColumn),i=n.indexOf(t.targetColumn);return r===-1||i===-1?void 0:{idIndex:r,targetIndex:i}}function C(e){return e.every(e=>e.trim()===``)}function w(e){let t=e.slice(0,10).map(e=>`\`${T(e)}\``);return e.length>10?`${t.join(`, `)}, ...`:t.join(`, `)}function T(e){let t=e.replaceAll(/[\p{Cc}]/gu,` `).replaceAll("`",`'`);return t.trim()===``?`(空)`:t.length>50?`${t.slice(0,50)}…`:t}function E(e,t){return t.decisionCode===i.DecisionCode.ACCEPTED==(e.expectedResult===`accepted`)}function D(e){return Number.isFinite(Number(e))&&e.trim()!==``?void 0:`数値ではありません`}function O(e){return e.reduce((e,t)=>e+t,0)/e.length}exports.evaluationJudgePreset=h,exports.evaluationMetrics=m;
11
+ //# sourceMappingURL=evaluation.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluation.cjs","names":["parseArgs","resolveCwds","checkProblemDirIsolation","path","judgeByStaticAnalysis","readProblemMarkdownFrontMatter","isSafeSubmissionOutputPath","fs","DecisionCode","parseCsvRecords","CsvParseError","parseCsv"],"sources":["../../src/presets/evaluation.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { checkProblemDirIsolation } from '../helpers/checkProblemDirIsolation.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { CsvParseError, type CsvParseErrorReason, parseCsv, parseCsvRecords } from '../helpers/parseCsv.js';\nimport { printDebugBanner } from '../helpers/printDebugBanner.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { isSafeSubmissionOutputPath } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport {\n printDebugCwdBanner,\n printDebugExpectationFailureBanner,\n resolveCwds,\n type ResolvedCwd,\n} from '../helpers/resolveCwds.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst DEFAULT_SUBMISSION_FILE_PATH = 'submission.csv';\nconst TEST_CASE_ID = 'evaluation';\nconst MAX_LISTED_IDS = 10;\n// Learner-controlled cells are echoed into the feedback, so clip them to keep the result line small.\nconst MAX_ECHOED_CELL_LENGTH = 50;\nconst CSV_PARSE_ERROR_MESSAGES: Record<CsvParseErrorReason, string> = {\n unterminated_quote: '引用符(`\"`)が閉じられていません',\n unexpected_quote: '引用符(`\"`)は値全体を囲む位置にしか書けません',\n text_after_quoted_field: '閉じ引用符の後に区切り文字以外の文字があります',\n};\n\nexport interface EvaluationMetric {\n /** Label shown with the score, e.g. `RMSLE`. */\n label: string;\n isHigherBetter: boolean;\n /**\n * Returns an error message when a predicted value cannot be scored (e.g. it is not a number).\n * The message is appended to the row's line number and the offending value, so phrase it as a\n * predicate such as `数値ではありません`. It also validates the ground truth when it is loaded.\n */\n validatePrediction?: (prediction: string) => string | undefined;\n /** Computes the score from predictions and answers aligned by id. Both are raw CSV cell strings. */\n compute: (predictions: readonly string[], answers: readonly string[]) => number;\n}\n\nexport interface EvaluationJudgePresetOptions {\n /** CSV file holding the hidden ground truth, relative to the problem directory. */\n answerFilePath: string;\n /** CSV file the submission must contain. Defaults to `submission.csv`. */\n submissionFilePath?: string;\n /** Column that identifies each row in both CSV files. */\n idColumn: string;\n /** Column holding the answer in the ground truth and the prediction in the submission. */\n targetColumn: string;\n metric: EvaluationMetric;\n /**\n * Score required for acceptance: the submission is accepted when its score is at least this value\n * for a higher-is-better metric and at most this value otherwise. Every valid submission is\n * accepted when omitted.\n */\n acceptableScore?: number;\n}\n\nexport const evaluationMetrics = {\n rmsle: {\n label: 'RMSLE',\n isHigherBetter: false,\n validatePrediction: (prediction: string) =>\n validateFiniteNumber(prediction) ?? (Number(prediction) < 0 ? '負の値です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(\n mean(\n predictions.map(\n (prediction, index) => (Math.log1p(Number(prediction)) - Math.log1p(Number(answers[index]))) ** 2\n )\n )\n ),\n },\n rmse: {\n label: 'RMSE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(mean(predictions.map((prediction, index) => (Number(prediction) - Number(answers[index])) ** 2))),\n },\n mae: {\n label: 'MAE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => Math.abs(Number(prediction) - Number(answers[index])))),\n },\n accuracy: {\n label: 'Accuracy',\n isHigherBetter: true,\n validatePrediction: (prediction: string) => (prediction.trim() === '' ? '空です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => (prediction.trim() === answers[index]?.trim() ? 1 : 0))),\n },\n} as const satisfies Record<string, EvaluationMetric>;\n\n/**\n * A preset for Kaggle-style model evaluation problems: the submission is a CSV of predictions for a\n * hidden test set, and the judge reports the metric value as `score` instead of comparing outputs.\n * The problem directory keeps the ground truth CSV, so it is never shipped to learners.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { evaluationJudgePreset, evaluationMetrics } from '@exercode/problem-utils/presets/evaluation';\n *\n * await evaluationJudgePreset(import.meta.dirname, {\n * answerFilePath: 'evaluation/answer.csv',\n * idColumn: 'Id',\n * targetColumn: 'TradePrice',\n * metric: evaluationMetrics.rmsle,\n * acceptableScore: 0.5,\n * });\n * ```\n *\n * Run `bun judge.ts model_answers/default` to judge one submission, or `bun judge.ts` to judge every\n * `<problemDir>/model_answers/*` and `<problemDir>/model_answers.fails/*` directory for debugging.\n */\nexport async function evaluationJudgePreset(problemDir: string, options: EvaluationJudgePresetOptions): Promise<void> {\n const args = parseArgs(process.argv);\n const { cwds, isDebugMode } = await resolveCwds(problemDir, args.cwd);\n\n if (isDebugMode) {\n const acceptedCwd = cwds.find((cwd) => cwd.expectedResult === 'accepted');\n if (acceptedCwd) {\n const isolationCheckResult = await checkProblemDirIsolation(problemDir, acceptedCwd, args.params);\n if (!isolationCheckResult.passed) {\n process.exitCode = 1;\n return;\n }\n } else {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'No accepted model answer is available for checking that the copied judge still accepts a valid submission.',\n ]);\n }\n }\n\n const answer = await readAnswer(path.join(problemDir, options.answerFilePath), options);\n for (const resolvedCwd of cwds) {\n if (isDebugMode) printDebugCwdBanner(problemDir, resolvedCwd);\n const result = await judgeSubmission(problemDir, resolvedCwd.cwd, answer, options);\n printTestCaseResult({ testCaseId: TEST_CASE_ID, ...result });\n if (isDebugMode && !matchesExpectedResult(resolvedCwd, result)) {\n process.exitCode = 1;\n printDebugExpectationFailureBanner(problemDir, resolvedCwd);\n }\n }\n}\n\nasync function judgeSubmission(\n problemDir: string,\n cwd: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Promise<Omit<TestCaseResult, 'testCaseId'>> {\n const staticAnalysisResult = await judgeByStaticAnalysis(cwd, await readProblemMarkdownFrontMatter(problemDir));\n if (staticAnalysisResult) return staticAnalysisResult;\n\n const submissionFilePath = options.submissionFilePath ?? DEFAULT_SUBMISSION_FILE_PATH;\n let submissionText: string;\n try {\n const resolvedPath = path.join(cwd, submissionFilePath);\n // The harness reads the file as the trusted user, so refuse symlinks escaping the submission.\n if (!(await isSafeSubmissionOutputPath(cwd, resolvedPath))) throw new Error('unsafe path');\n submissionText = await fs.readFile(resolvedPath, 'utf8');\n } catch {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n feedbackMarkdown: `\\`${submissionFilePath}\\`が見つかりません。予測結果のCSVファイルを\\`${submissionFilePath}\\`という名前で提出してください。`,\n };\n }\n\n const predictions = readPredictions(submissionText, submissionFilePath, answer, options);\n if (typeof predictions === 'string') {\n return { decisionCode: DecisionCode.WRONG_ANSWER, feedbackMarkdown: predictions };\n }\n\n const answerValues = [...answer.values()];\n const score = options.metric.compute(\n [...answer.keys()].map((id) => predictions.get(id) as string),\n answerValues\n );\n if (!Number.isFinite(score)) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n feedbackMarkdown: `予測値が極端なため、${options.metric.label}を計算できませんでした。予測値を見直してください。`,\n };\n }\n const isAccepted =\n options.acceptableScore === undefined ||\n (options.metric.isHigherBetter ? score >= options.acceptableScore : score <= options.acceptableScore);\n return {\n decisionCode: isAccepted ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER,\n score,\n scoreLabel: options.metric.label,\n feedbackMarkdown: buildScoreMarkdown(score, answerValues.length, isAccepted, options),\n };\n}\n\nfunction buildScoreMarkdown(\n score: number,\n rowCount: number,\n isAccepted: boolean,\n options: EvaluationJudgePresetOptions\n): string {\n const { label, isHigherBetter } = options.metric;\n const precision = findDistinguishingPrecision(score, options.acceptableScore);\n const acceptanceLine =\n options.acceptableScore === undefined\n ? ''\n : `\\n合格基準: ${label} ${isHigherBetter ? '≧' : '≦'} ${formatScore(options.acceptableScore, precision)}(${isAccepted ? '達成' : '未達成'})`;\n return `| 指標 | スコア |\n| ---- | ------ |\n| ${label} | ${formatScore(score, precision)} |\n\n評価件数: ${rowCount.toLocaleString('en-US')}件${acceptanceLine}\n`;\n}\n\n/**\n * Finds the number of significant digits at which the score and the threshold render differently\n * (or stop changing), so the feedback never shows a pair that contradicts the decision.\n */\nfunction findDistinguishingPrecision(score: number, threshold: number | undefined): number {\n const minimumPrecision = 6;\n if (threshold === undefined || score === threshold) return minimumPrecision;\n for (let precision = minimumPrecision; precision < 17; precision++) {\n if (formatScore(score, precision) !== formatScore(threshold, precision)) return precision;\n }\n return 17;\n}\n\nfunction formatScore(score: number, precision: number): string {\n return Number(score.toPrecision(precision)).toString();\n}\n\nfunction readPredictions(\n submissionText: string,\n submissionFilePath: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Map<string, string> | string {\n let records: ReturnType<typeof parseCsvRecords>;\n try {\n records = parseCsvRecords(submissionText);\n } catch (error) {\n const detail =\n error instanceof CsvParseError ? `${error.lineNumber}行目付近: ${CSV_PARSE_ERROR_MESSAGES[error.reason]}` : '';\n return `\\`${submissionFilePath}\\`をCSVとして読み込めませんでした。${detail}`;\n }\n const columns = findColumns(records[0]?.cells ?? [], options);\n if (!columns) {\n return `\\`${submissionFilePath}\\`の1行目には\\`${options.idColumn}\\`列と\\`${options.targetColumn}\\`列のヘッダーが必要です。`;\n }\n\n const predictions = new Map<string, string>();\n const problems: string[] = [];\n let problemCount = 0;\n const reportProblem = (problem: string): void => {\n problemCount++;\n if (problems.length < MAX_LISTED_IDS) problems.push(problem);\n };\n for (const { cells: row, lineNumber } of records.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const prediction = row[columns.targetIndex] ?? '';\n if (!answer.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行は評価対象ではありません`);\n } else if (predictions.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行が重複しています`);\n } else {\n const validationError = options.metric.validatePrediction?.(prediction);\n if (validationError) {\n reportProblem(`${lineNumber}行目: \\`${options.targetColumn}\\`の値\\`${clip(prediction)}\\`は${validationError}`);\n }\n predictions.set(id, prediction);\n }\n }\n const missingIds = [...answer.keys()].filter((id) => !predictions.has(id));\n if (missingIds.length > 0) {\n // Missing rows are the most actionable problem, so they must survive the listing cap.\n problemCount++;\n problems.unshift(\n `\\`${options.idColumn}\\`が${listIds(missingIds)}の行がありません(不足 ${missingIds.length.toLocaleString('en-US')}件)`\n );\n }\n if (problemCount > 0) {\n return `\\`${submissionFilePath}\\`の内容に問題があります。\n\n${problems.map((problem) => `- ${problem}`).join('\\n')}${problemCount > problems.length ? `\\n- ほか${(problemCount - problems.length).toLocaleString('en-US')}件` : ''}\n`;\n }\n return predictions;\n}\n\nasync function readAnswer(answerFilePath: string, options: EvaluationJudgePresetOptions): Promise<Map<string, string>> {\n const rows = parseCsv(await fs.readFile(answerFilePath, 'utf8'));\n const columns = findColumns(rows[0] ?? [], options);\n if (!columns) {\n throw new Error(`answer file must have ${options.idColumn} and ${options.targetColumn} columns: ${answerFilePath}`);\n }\n const answer = new Map<string, string>();\n for (const row of rows.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const value = row[columns.targetIndex] ?? '';\n if (!id) throw new Error(`empty ${options.idColumn} in answer file: ${answerFilePath}`);\n if (answer.has(id)) throw new Error(`duplicate ${options.idColumn} in answer file: ${id}`);\n const validationError = options.metric.validatePrediction?.(value);\n if (validationError)\n throw new Error(`invalid ${options.targetColumn} in answer file for ${id}: ${validationError}`);\n answer.set(id, value);\n }\n if (answer.size === 0) throw new Error(`answer file has no rows: ${answerFilePath}`);\n return answer;\n}\n\nfunction findColumns(\n header: readonly string[],\n options: EvaluationJudgePresetOptions\n): { idIndex: number; targetIndex: number } | undefined {\n const names = header.map((name) => name.trim());\n const idIndex = names.indexOf(options.idColumn);\n const targetIndex = names.indexOf(options.targetColumn);\n return idIndex === -1 || targetIndex === -1 ? undefined : { idIndex, targetIndex };\n}\n\nfunction isBlankRow(row: readonly string[]): boolean {\n return row.every((cell) => cell.trim() === '');\n}\n\nfunction listIds(ids: readonly string[]): string {\n const listed = ids.slice(0, MAX_LISTED_IDS).map((id) => `\\`${clip(id)}\\``);\n return ids.length > MAX_LISTED_IDS ? `${listed.join(', ')}, ...` : listed.join(', ');\n}\n\nfunction clip(value: string): string {\n // Control characters and backticks would break the markdown list item and inline code the value is echoed in.\n const printable = value.replaceAll(/[\\p{Cc}]/gu, ' ').replaceAll('`', \"'\");\n if (printable.trim() === '') return '(空)';\n return printable.length > MAX_ECHOED_CELL_LENGTH ? `${printable.slice(0, MAX_ECHOED_CELL_LENGTH)}…` : printable;\n}\n\nfunction matchesExpectedResult(resolvedCwd: ResolvedCwd, result: Pick<TestCaseResult, 'decisionCode'>): boolean {\n return (result.decisionCode === DecisionCode.ACCEPTED) === (resolvedCwd.expectedResult === 'accepted');\n}\n\nfunction validateFiniteNumber(prediction: string): string | undefined {\n return Number.isFinite(Number(prediction)) && prediction.trim() !== '' ? undefined : '数値ではありません';\n}\n\nfunction mean(values: readonly number[]): number {\n return values.reduce((sum, value) => sum + value, 0) / values.length;\n}\n"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ export interface EvaluationMetric {
2
+ /** Label shown with the score, e.g. `RMSLE`. */
3
+ label: string;
4
+ isHigherBetter: boolean;
5
+ /**
6
+ * Returns an error message when a predicted value cannot be scored (e.g. it is not a number).
7
+ * The message is appended to the row's line number and the offending value, so phrase it as a
8
+ * predicate such as `数値ではありません`. It also validates the ground truth when it is loaded.
9
+ */
10
+ validatePrediction?: (prediction: string) => string | undefined;
11
+ /** Computes the score from predictions and answers aligned by id. Both are raw CSV cell strings. */
12
+ compute: (predictions: readonly string[], answers: readonly string[]) => number;
13
+ }
14
+ export interface EvaluationJudgePresetOptions {
15
+ /** CSV file holding the hidden ground truth, relative to the problem directory. */
16
+ answerFilePath: string;
17
+ /** CSV file the submission must contain. Defaults to `submission.csv`. */
18
+ submissionFilePath?: string;
19
+ /** Column that identifies each row in both CSV files. */
20
+ idColumn: string;
21
+ /** Column holding the answer in the ground truth and the prediction in the submission. */
22
+ targetColumn: string;
23
+ metric: EvaluationMetric;
24
+ /**
25
+ * Score required for acceptance: the submission is accepted when its score is at least this value
26
+ * for a higher-is-better metric and at most this value otherwise. Every valid submission is
27
+ * accepted when omitted.
28
+ */
29
+ acceptableScore?: number;
30
+ }
31
+ export declare const evaluationMetrics: {
32
+ readonly rmsle: {
33
+ readonly label: 'RMSLE';
34
+ readonly isHigherBetter: false;
35
+ readonly validatePrediction: (prediction: string) => string | undefined;
36
+ readonly compute: (predictions: readonly string[], answers: readonly string[]) => number;
37
+ };
38
+ readonly rmse: {
39
+ readonly label: 'RMSE';
40
+ readonly isHigherBetter: false;
41
+ readonly validatePrediction: typeof validateFiniteNumber;
42
+ readonly compute: (predictions: readonly string[], answers: readonly string[]) => number;
43
+ };
44
+ readonly mae: {
45
+ readonly label: 'MAE';
46
+ readonly isHigherBetter: false;
47
+ readonly validatePrediction: typeof validateFiniteNumber;
48
+ readonly compute: (predictions: readonly string[], answers: readonly string[]) => number;
49
+ };
50
+ readonly accuracy: {
51
+ readonly label: 'Accuracy';
52
+ readonly isHigherBetter: true;
53
+ readonly validatePrediction: (prediction: string) => "空です" | undefined;
54
+ readonly compute: (predictions: readonly string[], answers: readonly string[]) => number;
55
+ };
56
+ };
57
+ /**
58
+ * A preset for Kaggle-style model evaluation problems: the submission is a CSV of predictions for a
59
+ * hidden test set, and the judge reports the metric value as `score` instead of comparing outputs.
60
+ * The problem directory keeps the ground truth CSV, so it is never shipped to learners.
61
+ *
62
+ * @example
63
+ * Create `judge.ts`:
64
+ * ```ts
65
+ * import { evaluationJudgePreset, evaluationMetrics } from '@exercode/problem-utils/presets/evaluation';
66
+ *
67
+ * await evaluationJudgePreset(import.meta.dirname, {
68
+ * answerFilePath: 'evaluation/answer.csv',
69
+ * idColumn: 'Id',
70
+ * targetColumn: 'TradePrice',
71
+ * metric: evaluationMetrics.rmsle,
72
+ * acceptableScore: 0.5,
73
+ * });
74
+ * ```
75
+ *
76
+ * Run `bun judge.ts model_answers/default` to judge one submission, or `bun judge.ts` to judge every
77
+ * `<problemDir>/model_answers/*` and `<problemDir>/model_answers.fails/*` directory for debugging.
78
+ */
79
+ export declare function evaluationJudgePreset(problemDir: string, options: EvaluationJudgePresetOptions): Promise<void>;
80
+ declare function validateFiniteNumber(prediction: string): string | undefined;
81
+ export {};
@@ -0,0 +1,11 @@
1
+ import{parseArgs as e}from"../helpers/parseArgs.js";import{CsvParseError as t,parseCsv as n,parseCsvRecords as r}from"../helpers/parseCsv.js";import{printTestCaseResult as i}from"../helpers/printTestCaseResult.js";import{DecisionCode as a}from"../types/decisionCode.js";import{printDebugBanner as o}from"../helpers/printDebugBanner.js";import{checkProblemDirIsolation as s}from"../helpers/checkProblemDirIsolation.js";import{judgeByStaticAnalysis as c}from"../helpers/judgeByStaticAnalysis.js";import{isSafeSubmissionOutputPath as l}from"../helpers/readOutputFiles.js";import{readProblemMarkdownFrontMatter as u}from"../helpers/readProblemMarkdownFrontMatter.js";import{printDebugCwdBanner as d,printDebugExpectationFailureBanner as f,resolveCwds as p}from"../helpers/resolveCwds.js";import m from"node:fs/promises";import h from"node:path";const g={unterminated_quote:'引用符(`"`)が閉じられていません',unexpected_quote:'引用符(`"`)は値全体を囲む位置にしか書けません',text_after_quoted_field:`閉じ引用符の後に区切り文字以外の文字があります`},_={rmsle:{label:`RMSLE`,isHigherBetter:!1,validatePrediction:e=>A(e)??(Number(e)<0?`負の値です`:void 0),compute:(e,t)=>Math.sqrt(j(e.map((e,n)=>(Math.log1p(Number(e))-Math.log1p(Number(t[n])))**2)))},rmse:{label:`RMSE`,isHigherBetter:!1,validatePrediction:A,compute:(e,t)=>Math.sqrt(j(e.map((e,n)=>(Number(e)-Number(t[n]))**2)))},mae:{label:`MAE`,isHigherBetter:!1,validatePrediction:A,compute:(e,t)=>j(e.map((e,n)=>Math.abs(Number(e)-Number(t[n]))))},accuracy:{label:`Accuracy`,isHigherBetter:!0,validatePrediction:e=>e.trim()===``?`空です`:void 0,compute:(e,t)=>j(e.map((e,n)=>+(e.trim()===t[n]?.trim())))}};async function v(t,n){let r=e(process.argv),{cwds:a,isDebugMode:c}=await p(t,r.cwd);if(c){let e=a.find(e=>e.expectedResult===`accepted`);if(e){if(!(await s(t,e,r.params)).passed){process.exitCode=1;return}}else o([`[DEBUG MODE] isolated problem directory check skipped`,``,`No accepted model answer is available for checking that the copied judge still accepts a valid submission.`])}let l=await w(h.join(t,n.answerFilePath),n);for(let e of a){c&&d(t,e);let r=await y(t,e.cwd,l,n);i({testCaseId:`evaluation`,...r}),c&&!k(e,r)&&(process.exitCode=1,f(t,e))}}async function y(e,t,n,r){let i=await c(t,await u(e));if(i)return i;let o=r.submissionFilePath??`submission.csv`,s;try{let e=h.join(t,o);if(!await l(t,e))throw Error(`unsafe path`);s=await m.readFile(e,`utf8`)}catch{return{decisionCode:a.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`\`${o}\`が見つかりません。予測結果のCSVファイルを\`${o}\`という名前で提出してください。`}}let d=C(s,o,n,r);if(typeof d==`string`)return{decisionCode:a.WRONG_ANSWER,feedbackMarkdown:d};let f=[...n.values()],p=r.metric.compute([...n.keys()].map(e=>d.get(e)),f);if(!Number.isFinite(p))return{decisionCode:a.WRONG_ANSWER,feedbackMarkdown:`予測値が極端なため、${r.metric.label}を計算できませんでした。予測値を見直してください。`};let g=r.acceptableScore===void 0||(r.metric.isHigherBetter?p>=r.acceptableScore:p<=r.acceptableScore);return{decisionCode:g?a.ACCEPTED:a.WRONG_ANSWER,score:p,scoreLabel:r.metric.label,feedbackMarkdown:b(p,f.length,g,r)}}function b(e,t,n,r){let{label:i,isHigherBetter:a}=r.metric,o=x(e,r.acceptableScore),s=r.acceptableScore===void 0?``:`\n合格基準: ${i} ${a?`≧`:`≦`} ${S(r.acceptableScore,o)}(${n?`達成`:`未達成`})`;return`| 指標 | スコア |
2
+ | ---- | ------ |
3
+ | ${i} | ${S(e,o)} |
4
+
5
+ 評価件数: ${t.toLocaleString(`en-US`)}件${s}
6
+ `}function x(e,t){if(t===void 0||e===t)return 6;for(let n=6;n<17;n++)if(S(e,n)!==S(t,n))return n;return 17}function S(e,t){return Number(e.toPrecision(t)).toString()}function C(e,n,i,a){let o;try{o=r(e)}catch(e){return`\`${n}\`をCSVとして読み込めませんでした。${e instanceof t?`${e.lineNumber}行目付近: ${g[e.reason]}`:``}`}let s=T(o[0]?.cells??[],a);if(!s)return`\`${n}\`の1行目には\`${a.idColumn}\`列と\`${a.targetColumn}\`列のヘッダーが必要です。`;let c=new Map,l=[],u=0,d=e=>{u++,l.length<10&&l.push(e)};for(let{cells:e,lineNumber:t}of o.slice(1)){if(E(e))continue;let n=e[s.idIndex]?.trim()??``,r=e[s.targetIndex]??``;if(!i.has(n))d(`${t}行目: \`${a.idColumn}\`が\`${O(n)}\`の行は評価対象ではありません`);else if(c.has(n))d(`${t}行目: \`${a.idColumn}\`が\`${O(n)}\`の行が重複しています`);else{let e=a.metric.validatePrediction?.(r);e&&d(`${t}行目: \`${a.targetColumn}\`の値\`${O(r)}\`は${e}`),c.set(n,r)}}let f=[...i.keys()].filter(e=>!c.has(e));return f.length>0&&(u++,l.unshift(`\`${a.idColumn}\`が${D(f)}の行がありません(不足 ${f.length.toLocaleString(`en-US`)}件)`)),u>0?`\`${n}\`の内容に問題があります。
7
+
8
+ ${l.map(e=>`- ${e}`).join(`
9
+ `)}${u>l.length?`\n- ほか${(u-l.length).toLocaleString(`en-US`)}件`:``}
10
+ `:c}async function w(e,t){let r=n(await m.readFile(e,`utf8`)),i=T(r[0]??[],t);if(!i)throw Error(`answer file must have ${t.idColumn} and ${t.targetColumn} columns: ${e}`);let a=new Map;for(let n of r.slice(1)){if(E(n))continue;let r=n[i.idIndex]?.trim()??``,o=n[i.targetIndex]??``;if(!r)throw Error(`empty ${t.idColumn} in answer file: ${e}`);if(a.has(r))throw Error(`duplicate ${t.idColumn} in answer file: ${r}`);let s=t.metric.validatePrediction?.(o);if(s)throw Error(`invalid ${t.targetColumn} in answer file for ${r}: ${s}`);a.set(r,o)}if(a.size===0)throw Error(`answer file has no rows: ${e}`);return a}function T(e,t){let n=e.map(e=>e.trim()),r=n.indexOf(t.idColumn),i=n.indexOf(t.targetColumn);return r===-1||i===-1?void 0:{idIndex:r,targetIndex:i}}function E(e){return e.every(e=>e.trim()===``)}function D(e){let t=e.slice(0,10).map(e=>`\`${O(e)}\``);return e.length>10?`${t.join(`, `)}, ...`:t.join(`, `)}function O(e){let t=e.replaceAll(/[\p{Cc}]/gu,` `).replaceAll("`",`'`);return t.trim()===``?`(空)`:t.length>50?`${t.slice(0,50)}…`:t}function k(e,t){return t.decisionCode===a.ACCEPTED==(e.expectedResult===`accepted`)}function A(e){return Number.isFinite(Number(e))&&e.trim()!==``?void 0:`数値ではありません`}function j(e){return e.reduce((e,t)=>e+t,0)/e.length}export{v as evaluationJudgePreset,_ as evaluationMetrics};
11
+ //# sourceMappingURL=evaluation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluation.js","names":[],"sources":["../../src/presets/evaluation.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { checkProblemDirIsolation } from '../helpers/checkProblemDirIsolation.js';\nimport { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { CsvParseError, type CsvParseErrorReason, parseCsv, parseCsvRecords } from '../helpers/parseCsv.js';\nimport { printDebugBanner } from '../helpers/printDebugBanner.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { isSafeSubmissionOutputPath } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport {\n printDebugCwdBanner,\n printDebugExpectationFailureBanner,\n resolveCwds,\n type ResolvedCwd,\n} from '../helpers/resolveCwds.js';\nimport { DecisionCode } from '../types/decisionCode.js';\nimport type { TestCaseResult } from '../types/testCaseResult.js';\n\nconst DEFAULT_SUBMISSION_FILE_PATH = 'submission.csv';\nconst TEST_CASE_ID = 'evaluation';\nconst MAX_LISTED_IDS = 10;\n// Learner-controlled cells are echoed into the feedback, so clip them to keep the result line small.\nconst MAX_ECHOED_CELL_LENGTH = 50;\nconst CSV_PARSE_ERROR_MESSAGES: Record<CsvParseErrorReason, string> = {\n unterminated_quote: '引用符(`\"`)が閉じられていません',\n unexpected_quote: '引用符(`\"`)は値全体を囲む位置にしか書けません',\n text_after_quoted_field: '閉じ引用符の後に区切り文字以外の文字があります',\n};\n\nexport interface EvaluationMetric {\n /** Label shown with the score, e.g. `RMSLE`. */\n label: string;\n isHigherBetter: boolean;\n /**\n * Returns an error message when a predicted value cannot be scored (e.g. it is not a number).\n * The message is appended to the row's line number and the offending value, so phrase it as a\n * predicate such as `数値ではありません`. It also validates the ground truth when it is loaded.\n */\n validatePrediction?: (prediction: string) => string | undefined;\n /** Computes the score from predictions and answers aligned by id. Both are raw CSV cell strings. */\n compute: (predictions: readonly string[], answers: readonly string[]) => number;\n}\n\nexport interface EvaluationJudgePresetOptions {\n /** CSV file holding the hidden ground truth, relative to the problem directory. */\n answerFilePath: string;\n /** CSV file the submission must contain. Defaults to `submission.csv`. */\n submissionFilePath?: string;\n /** Column that identifies each row in both CSV files. */\n idColumn: string;\n /** Column holding the answer in the ground truth and the prediction in the submission. */\n targetColumn: string;\n metric: EvaluationMetric;\n /**\n * Score required for acceptance: the submission is accepted when its score is at least this value\n * for a higher-is-better metric and at most this value otherwise. Every valid submission is\n * accepted when omitted.\n */\n acceptableScore?: number;\n}\n\nexport const evaluationMetrics = {\n rmsle: {\n label: 'RMSLE',\n isHigherBetter: false,\n validatePrediction: (prediction: string) =>\n validateFiniteNumber(prediction) ?? (Number(prediction) < 0 ? '負の値です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(\n mean(\n predictions.map(\n (prediction, index) => (Math.log1p(Number(prediction)) - Math.log1p(Number(answers[index]))) ** 2\n )\n )\n ),\n },\n rmse: {\n label: 'RMSE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n Math.sqrt(mean(predictions.map((prediction, index) => (Number(prediction) - Number(answers[index])) ** 2))),\n },\n mae: {\n label: 'MAE',\n isHigherBetter: false,\n validatePrediction: validateFiniteNumber,\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => Math.abs(Number(prediction) - Number(answers[index])))),\n },\n accuracy: {\n label: 'Accuracy',\n isHigherBetter: true,\n validatePrediction: (prediction: string) => (prediction.trim() === '' ? '空です' : undefined),\n compute: (predictions: readonly string[], answers: readonly string[]) =>\n mean(predictions.map((prediction, index) => (prediction.trim() === answers[index]?.trim() ? 1 : 0))),\n },\n} as const satisfies Record<string, EvaluationMetric>;\n\n/**\n * A preset for Kaggle-style model evaluation problems: the submission is a CSV of predictions for a\n * hidden test set, and the judge reports the metric value as `score` instead of comparing outputs.\n * The problem directory keeps the ground truth CSV, so it is never shipped to learners.\n *\n * @example\n * Create `judge.ts`:\n * ```ts\n * import { evaluationJudgePreset, evaluationMetrics } from '@exercode/problem-utils/presets/evaluation';\n *\n * await evaluationJudgePreset(import.meta.dirname, {\n * answerFilePath: 'evaluation/answer.csv',\n * idColumn: 'Id',\n * targetColumn: 'TradePrice',\n * metric: evaluationMetrics.rmsle,\n * acceptableScore: 0.5,\n * });\n * ```\n *\n * Run `bun judge.ts model_answers/default` to judge one submission, or `bun judge.ts` to judge every\n * `<problemDir>/model_answers/*` and `<problemDir>/model_answers.fails/*` directory for debugging.\n */\nexport async function evaluationJudgePreset(problemDir: string, options: EvaluationJudgePresetOptions): Promise<void> {\n const args = parseArgs(process.argv);\n const { cwds, isDebugMode } = await resolveCwds(problemDir, args.cwd);\n\n if (isDebugMode) {\n const acceptedCwd = cwds.find((cwd) => cwd.expectedResult === 'accepted');\n if (acceptedCwd) {\n const isolationCheckResult = await checkProblemDirIsolation(problemDir, acceptedCwd, args.params);\n if (!isolationCheckResult.passed) {\n process.exitCode = 1;\n return;\n }\n } else {\n printDebugBanner([\n '[DEBUG MODE] isolated problem directory check skipped',\n '',\n 'No accepted model answer is available for checking that the copied judge still accepts a valid submission.',\n ]);\n }\n }\n\n const answer = await readAnswer(path.join(problemDir, options.answerFilePath), options);\n for (const resolvedCwd of cwds) {\n if (isDebugMode) printDebugCwdBanner(problemDir, resolvedCwd);\n const result = await judgeSubmission(problemDir, resolvedCwd.cwd, answer, options);\n printTestCaseResult({ testCaseId: TEST_CASE_ID, ...result });\n if (isDebugMode && !matchesExpectedResult(resolvedCwd, result)) {\n process.exitCode = 1;\n printDebugExpectationFailureBanner(problemDir, resolvedCwd);\n }\n }\n}\n\nasync function judgeSubmission(\n problemDir: string,\n cwd: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Promise<Omit<TestCaseResult, 'testCaseId'>> {\n const staticAnalysisResult = await judgeByStaticAnalysis(cwd, await readProblemMarkdownFrontMatter(problemDir));\n if (staticAnalysisResult) return staticAnalysisResult;\n\n const submissionFilePath = options.submissionFilePath ?? DEFAULT_SUBMISSION_FILE_PATH;\n let submissionText: string;\n try {\n const resolvedPath = path.join(cwd, submissionFilePath);\n // The harness reads the file as the trusted user, so refuse symlinks escaping the submission.\n if (!(await isSafeSubmissionOutputPath(cwd, resolvedPath))) throw new Error('unsafe path');\n submissionText = await fs.readFile(resolvedPath, 'utf8');\n } catch {\n return {\n decisionCode: DecisionCode.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,\n feedbackMarkdown: `\\`${submissionFilePath}\\`が見つかりません。予測結果のCSVファイルを\\`${submissionFilePath}\\`という名前で提出してください。`,\n };\n }\n\n const predictions = readPredictions(submissionText, submissionFilePath, answer, options);\n if (typeof predictions === 'string') {\n return { decisionCode: DecisionCode.WRONG_ANSWER, feedbackMarkdown: predictions };\n }\n\n const answerValues = [...answer.values()];\n const score = options.metric.compute(\n [...answer.keys()].map((id) => predictions.get(id) as string),\n answerValues\n );\n if (!Number.isFinite(score)) {\n return {\n decisionCode: DecisionCode.WRONG_ANSWER,\n feedbackMarkdown: `予測値が極端なため、${options.metric.label}を計算できませんでした。予測値を見直してください。`,\n };\n }\n const isAccepted =\n options.acceptableScore === undefined ||\n (options.metric.isHigherBetter ? score >= options.acceptableScore : score <= options.acceptableScore);\n return {\n decisionCode: isAccepted ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER,\n score,\n scoreLabel: options.metric.label,\n feedbackMarkdown: buildScoreMarkdown(score, answerValues.length, isAccepted, options),\n };\n}\n\nfunction buildScoreMarkdown(\n score: number,\n rowCount: number,\n isAccepted: boolean,\n options: EvaluationJudgePresetOptions\n): string {\n const { label, isHigherBetter } = options.metric;\n const precision = findDistinguishingPrecision(score, options.acceptableScore);\n const acceptanceLine =\n options.acceptableScore === undefined\n ? ''\n : `\\n合格基準: ${label} ${isHigherBetter ? '≧' : '≦'} ${formatScore(options.acceptableScore, precision)}(${isAccepted ? '達成' : '未達成'})`;\n return `| 指標 | スコア |\n| ---- | ------ |\n| ${label} | ${formatScore(score, precision)} |\n\n評価件数: ${rowCount.toLocaleString('en-US')}件${acceptanceLine}\n`;\n}\n\n/**\n * Finds the number of significant digits at which the score and the threshold render differently\n * (or stop changing), so the feedback never shows a pair that contradicts the decision.\n */\nfunction findDistinguishingPrecision(score: number, threshold: number | undefined): number {\n const minimumPrecision = 6;\n if (threshold === undefined || score === threshold) return minimumPrecision;\n for (let precision = minimumPrecision; precision < 17; precision++) {\n if (formatScore(score, precision) !== formatScore(threshold, precision)) return precision;\n }\n return 17;\n}\n\nfunction formatScore(score: number, precision: number): string {\n return Number(score.toPrecision(precision)).toString();\n}\n\nfunction readPredictions(\n submissionText: string,\n submissionFilePath: string,\n answer: ReadonlyMap<string, string>,\n options: EvaluationJudgePresetOptions\n): Map<string, string> | string {\n let records: ReturnType<typeof parseCsvRecords>;\n try {\n records = parseCsvRecords(submissionText);\n } catch (error) {\n const detail =\n error instanceof CsvParseError ? `${error.lineNumber}行目付近: ${CSV_PARSE_ERROR_MESSAGES[error.reason]}` : '';\n return `\\`${submissionFilePath}\\`をCSVとして読み込めませんでした。${detail}`;\n }\n const columns = findColumns(records[0]?.cells ?? [], options);\n if (!columns) {\n return `\\`${submissionFilePath}\\`の1行目には\\`${options.idColumn}\\`列と\\`${options.targetColumn}\\`列のヘッダーが必要です。`;\n }\n\n const predictions = new Map<string, string>();\n const problems: string[] = [];\n let problemCount = 0;\n const reportProblem = (problem: string): void => {\n problemCount++;\n if (problems.length < MAX_LISTED_IDS) problems.push(problem);\n };\n for (const { cells: row, lineNumber } of records.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const prediction = row[columns.targetIndex] ?? '';\n if (!answer.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行は評価対象ではありません`);\n } else if (predictions.has(id)) {\n reportProblem(`${lineNumber}行目: \\`${options.idColumn}\\`が\\`${clip(id)}\\`の行が重複しています`);\n } else {\n const validationError = options.metric.validatePrediction?.(prediction);\n if (validationError) {\n reportProblem(`${lineNumber}行目: \\`${options.targetColumn}\\`の値\\`${clip(prediction)}\\`は${validationError}`);\n }\n predictions.set(id, prediction);\n }\n }\n const missingIds = [...answer.keys()].filter((id) => !predictions.has(id));\n if (missingIds.length > 0) {\n // Missing rows are the most actionable problem, so they must survive the listing cap.\n problemCount++;\n problems.unshift(\n `\\`${options.idColumn}\\`が${listIds(missingIds)}の行がありません(不足 ${missingIds.length.toLocaleString('en-US')}件)`\n );\n }\n if (problemCount > 0) {\n return `\\`${submissionFilePath}\\`の内容に問題があります。\n\n${problems.map((problem) => `- ${problem}`).join('\\n')}${problemCount > problems.length ? `\\n- ほか${(problemCount - problems.length).toLocaleString('en-US')}件` : ''}\n`;\n }\n return predictions;\n}\n\nasync function readAnswer(answerFilePath: string, options: EvaluationJudgePresetOptions): Promise<Map<string, string>> {\n const rows = parseCsv(await fs.readFile(answerFilePath, 'utf8'));\n const columns = findColumns(rows[0] ?? [], options);\n if (!columns) {\n throw new Error(`answer file must have ${options.idColumn} and ${options.targetColumn} columns: ${answerFilePath}`);\n }\n const answer = new Map<string, string>();\n for (const row of rows.slice(1)) {\n if (isBlankRow(row)) continue;\n const id = row[columns.idIndex]?.trim() ?? '';\n const value = row[columns.targetIndex] ?? '';\n if (!id) throw new Error(`empty ${options.idColumn} in answer file: ${answerFilePath}`);\n if (answer.has(id)) throw new Error(`duplicate ${options.idColumn} in answer file: ${id}`);\n const validationError = options.metric.validatePrediction?.(value);\n if (validationError)\n throw new Error(`invalid ${options.targetColumn} in answer file for ${id}: ${validationError}`);\n answer.set(id, value);\n }\n if (answer.size === 0) throw new Error(`answer file has no rows: ${answerFilePath}`);\n return answer;\n}\n\nfunction findColumns(\n header: readonly string[],\n options: EvaluationJudgePresetOptions\n): { idIndex: number; targetIndex: number } | undefined {\n const names = header.map((name) => name.trim());\n const idIndex = names.indexOf(options.idColumn);\n const targetIndex = names.indexOf(options.targetColumn);\n return idIndex === -1 || targetIndex === -1 ? undefined : { idIndex, targetIndex };\n}\n\nfunction isBlankRow(row: readonly string[]): boolean {\n return row.every((cell) => cell.trim() === '');\n}\n\nfunction listIds(ids: readonly string[]): string {\n const listed = ids.slice(0, MAX_LISTED_IDS).map((id) => `\\`${clip(id)}\\``);\n return ids.length > MAX_LISTED_IDS ? `${listed.join(', ')}, ...` : listed.join(', ');\n}\n\nfunction clip(value: string): string {\n // Control characters and backticks would break the markdown list item and inline code the value is echoed in.\n const printable = value.replaceAll(/[\\p{Cc}]/gu, ' ').replaceAll('`', \"'\");\n if (printable.trim() === '') return '(空)';\n return printable.length > MAX_ECHOED_CELL_LENGTH ? `${printable.slice(0, MAX_ECHOED_CELL_LENGTH)}…` : printable;\n}\n\nfunction matchesExpectedResult(resolvedCwd: ResolvedCwd, result: Pick<TestCaseResult, 'decisionCode'>): boolean {\n return (result.decisionCode === DecisionCode.ACCEPTED) === (resolvedCwd.expectedResult === 'accepted');\n}\n\nfunction validateFiniteNumber(prediction: string): string | undefined {\n return Number.isFinite(Number(prediction)) && prediction.trim() !== '' ? undefined : '数値ではありません';\n}\n\nfunction mean(values: readonly number[]): number {\n return values.reduce((sum, value) => sum + value, 0) / values.length;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";let e=require("zod");const t=e.z.object({path:e.z.string(),data:e.z.string(),encoding:e.z.literal(`base64`).optional()}),n=e.z.object({testCaseId:e.z.string(),decisionCode:e.z.number().int(),exitStatus:e.z.number().int().optional(),stdin:e.z.string().optional(),stdout:e.z.string().optional(),stderr:e.z.string().optional(),timeSeconds:e.z.number().optional(),memoryBytes:e.z.number().optional(),feedbackMarkdown:e.z.string().optional(),outputFiles:e.z.array(t).optional()});exports.TEST_CASE_RESULT_PREFIX=`TEST_CASE_RESULT `,exports.testCaseResultSchema=n;
1
+ "use strict";let e=require("zod");const t=e.z.object({path:e.z.string(),data:e.z.string(),encoding:e.z.literal(`base64`).optional()}),n=e.z.object({testCaseId:e.z.string(),decisionCode:e.z.number().int(),exitStatus:e.z.number().int().optional(),stdin:e.z.string().optional(),stdout:e.z.string().optional(),stderr:e.z.string().optional(),timeSeconds:e.z.number().optional(),memoryBytes:e.z.number().optional(),feedbackMarkdown:e.z.string().optional(),score:e.z.number().optional(),scoreLabel:e.z.string().optional(),outputFiles:e.z.array(t).optional()});exports.TEST_CASE_RESULT_PREFIX=`TEST_CASE_RESULT `,exports.testCaseResultSchema=n;
2
2
  //# sourceMappingURL=testCaseResult.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"testCaseResult.cjs","names":["z"],"sources":["../../src/types/testCaseResult.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const TEST_CASE_RESULT_PREFIX = 'TEST_CASE_RESULT ';\n\nconst testCaseResultOutputFileSchema = z.object({\n path: z.string(),\n data: z.string(),\n encoding: z.literal('base64').optional(),\n});\n\nexport const testCaseResultSchema = z.object({\n testCaseId: z.string(),\n decisionCode: z.number().int(),\n exitStatus: z.number().int().optional(),\n stdin: z.string().optional(),\n stdout: z.string().optional(),\n stderr: z.string().optional(),\n timeSeconds: z.number().optional(),\n memoryBytes: z.number().optional(),\n feedbackMarkdown: z.string().optional(),\n outputFiles: z.array(testCaseResultOutputFileSchema).optional(),\n});\n\nexport type TestCaseResult = z.infer<typeof testCaseResultSchema>;\n"],"mappings":"kCAEA,MAEM,EAAiCA,EAAAA,EAAE,OAAO,CAC9C,KAAMA,EAAAA,EAAE,OAAO,EACf,KAAMA,EAAAA,EAAE,OAAO,EACf,SAAUA,EAAAA,EAAE,QAAQ,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EAEY,EAAuBA,EAAAA,EAAE,OAAO,CAC3C,WAAYA,EAAAA,EAAE,OAAO,EACrB,aAAcA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,EAC7B,WAAYA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EACtC,MAAOA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAC3B,OAAQA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,OAAQA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,iBAAkBA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EACtC,YAAaA,EAAAA,EAAE,MAAM,CAA8B,CAAC,CAAC,SAAS,CAChE,CAAC"}
1
+ {"version":3,"file":"testCaseResult.cjs","names":["z"],"sources":["../../src/types/testCaseResult.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const TEST_CASE_RESULT_PREFIX = 'TEST_CASE_RESULT ';\n\nconst testCaseResultOutputFileSchema = z.object({\n path: z.string(),\n data: z.string(),\n encoding: z.literal('base64').optional(),\n});\n\nexport const testCaseResultSchema = z.object({\n testCaseId: z.string(),\n decisionCode: z.number().int(),\n exitStatus: z.number().int().optional(),\n stdin: z.string().optional(),\n stdout: z.string().optional(),\n stderr: z.string().optional(),\n timeSeconds: z.number().optional(),\n memoryBytes: z.number().optional(),\n feedbackMarkdown: z.string().optional(),\n /** Numeric score of a model-evaluation submission (e.g. an RMSLE value). */\n score: z.number().optional(),\n /** Label of `score` shown to learners (e.g. `RMSLE`). */\n scoreLabel: z.string().optional(),\n outputFiles: z.array(testCaseResultOutputFileSchema).optional(),\n});\n\nexport type TestCaseResult = z.infer<typeof testCaseResultSchema>;\n"],"mappings":"kCAEA,MAEM,EAAiCA,EAAAA,EAAE,OAAO,CAC9C,KAAMA,EAAAA,EAAE,OAAO,EACf,KAAMA,EAAAA,EAAE,OAAO,EACf,SAAUA,EAAAA,EAAE,QAAQ,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EAEY,EAAuBA,EAAAA,EAAE,OAAO,CAC3C,WAAYA,EAAAA,EAAE,OAAO,EACrB,aAAcA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,EAC7B,WAAYA,EAAAA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EACtC,MAAOA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAC3B,OAAQA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,OAAQA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,YAAaA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,iBAAkBA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAEtC,MAAOA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAE3B,WAAYA,EAAAA,EAAE,OAAO,CAAC,CAAC,SAAS,EAChC,YAAaA,EAAAA,EAAE,MAAM,CAA8B,CAAC,CAAC,SAAS,CAChE,CAAC"}
@@ -10,6 +10,8 @@ export declare const testCaseResultSchema: z.ZodObject<{
10
10
  timeSeconds: z.ZodOptional<z.ZodNumber>;
11
11
  memoryBytes: z.ZodOptional<z.ZodNumber>;
12
12
  feedbackMarkdown: z.ZodOptional<z.ZodString>;
13
+ score: z.ZodOptional<z.ZodNumber>;
14
+ scoreLabel: z.ZodOptional<z.ZodString>;
13
15
  outputFiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
14
16
  path: z.ZodString;
15
17
  data: z.ZodString;
@@ -1,2 +1,2 @@
1
- import{z as e}from"zod";const t=`TEST_CASE_RESULT `,n=e.object({path:e.string(),data:e.string(),encoding:e.literal(`base64`).optional()}),r=e.object({testCaseId:e.string(),decisionCode:e.number().int(),exitStatus:e.number().int().optional(),stdin:e.string().optional(),stdout:e.string().optional(),stderr:e.string().optional(),timeSeconds:e.number().optional(),memoryBytes:e.number().optional(),feedbackMarkdown:e.string().optional(),outputFiles:e.array(n).optional()});export{t as TEST_CASE_RESULT_PREFIX,r as testCaseResultSchema};
1
+ import{z as e}from"zod";const t=`TEST_CASE_RESULT `,n=e.object({path:e.string(),data:e.string(),encoding:e.literal(`base64`).optional()}),r=e.object({testCaseId:e.string(),decisionCode:e.number().int(),exitStatus:e.number().int().optional(),stdin:e.string().optional(),stdout:e.string().optional(),stderr:e.string().optional(),timeSeconds:e.number().optional(),memoryBytes:e.number().optional(),feedbackMarkdown:e.string().optional(),score:e.number().optional(),scoreLabel:e.string().optional(),outputFiles:e.array(n).optional()});export{t as TEST_CASE_RESULT_PREFIX,r as testCaseResultSchema};
2
2
  //# sourceMappingURL=testCaseResult.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testCaseResult.js","names":[],"sources":["../../src/types/testCaseResult.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const TEST_CASE_RESULT_PREFIX = 'TEST_CASE_RESULT ';\n\nconst testCaseResultOutputFileSchema = z.object({\n path: z.string(),\n data: z.string(),\n encoding: z.literal('base64').optional(),\n});\n\nexport const testCaseResultSchema = z.object({\n testCaseId: z.string(),\n decisionCode: z.number().int(),\n exitStatus: z.number().int().optional(),\n stdin: z.string().optional(),\n stdout: z.string().optional(),\n stderr: z.string().optional(),\n timeSeconds: z.number().optional(),\n memoryBytes: z.number().optional(),\n feedbackMarkdown: z.string().optional(),\n outputFiles: z.array(testCaseResultOutputFileSchema).optional(),\n});\n\nexport type TestCaseResult = z.infer<typeof testCaseResultSchema>;\n"],"mappings":"wBAEA,MAAa,EAA0B,oBAEjC,EAAiC,EAAE,OAAO,CAC9C,KAAM,EAAE,OAAO,EACf,KAAM,EAAE,OAAO,EACf,SAAU,EAAE,QAAQ,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EAEY,EAAuB,EAAE,OAAO,CAC3C,WAAY,EAAE,OAAO,EACrB,aAAc,EAAE,OAAO,CAAC,CAAC,IAAI,EAC7B,WAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EACtC,MAAO,EAAE,OAAO,CAAC,CAAC,SAAS,EAC3B,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,YAAa,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,YAAa,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,iBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS,EACtC,YAAa,EAAE,MAAM,CAA8B,CAAC,CAAC,SAAS,CAChE,CAAC"}
1
+ {"version":3,"file":"testCaseResult.js","names":[],"sources":["../../src/types/testCaseResult.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const TEST_CASE_RESULT_PREFIX = 'TEST_CASE_RESULT ';\n\nconst testCaseResultOutputFileSchema = z.object({\n path: z.string(),\n data: z.string(),\n encoding: z.literal('base64').optional(),\n});\n\nexport const testCaseResultSchema = z.object({\n testCaseId: z.string(),\n decisionCode: z.number().int(),\n exitStatus: z.number().int().optional(),\n stdin: z.string().optional(),\n stdout: z.string().optional(),\n stderr: z.string().optional(),\n timeSeconds: z.number().optional(),\n memoryBytes: z.number().optional(),\n feedbackMarkdown: z.string().optional(),\n /** Numeric score of a model-evaluation submission (e.g. an RMSLE value). */\n score: z.number().optional(),\n /** Label of `score` shown to learners (e.g. `RMSLE`). */\n scoreLabel: z.string().optional(),\n outputFiles: z.array(testCaseResultOutputFileSchema).optional(),\n});\n\nexport type TestCaseResult = z.infer<typeof testCaseResultSchema>;\n"],"mappings":"wBAEA,MAAa,EAA0B,oBAEjC,EAAiC,EAAE,OAAO,CAC9C,KAAM,EAAE,OAAO,EACf,KAAM,EAAE,OAAO,EACf,SAAU,EAAE,QAAQ,QAAQ,CAAC,CAAC,SAAS,CACzC,CAAC,EAEY,EAAuB,EAAE,OAAO,CAC3C,WAAY,EAAE,OAAO,EACrB,aAAc,EAAE,OAAO,CAAC,CAAC,IAAI,EAC7B,WAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EACtC,MAAO,EAAE,OAAO,CAAC,CAAC,SAAS,EAC3B,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,EAC5B,YAAa,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,YAAa,EAAE,OAAO,CAAC,CAAC,SAAS,EACjC,iBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS,EAEtC,MAAO,EAAE,OAAO,CAAC,CAAC,SAAS,EAE3B,WAAY,EAAE,OAAO,CAAC,CAAC,SAAS,EAChC,YAAa,EAAE,MAAM,CAA8B,CAAC,CAAC,SAAS,CAChE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exercode/problem-utils",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": ":100: A set of utilities for judging programs on Exercode (https://exercode.willbooster.com/).",
5
5
  "keywords": [
6
6
  "exercode",
@@ -53,11 +53,11 @@
53
53
  },
54
54
  "prettier": "@willbooster/prettier-config",
55
55
  "dependencies": {
56
- "@ai-sdk/amazon-bedrock": "5.0.57",
57
- "@ai-sdk/google": "4.0.44",
58
- "@ai-sdk/openai": "4.0.42",
59
- "@ai-sdk/xai": "4.0.39",
60
- "ai": "7.0.66",
56
+ "@ai-sdk/amazon-bedrock": "5.0.61",
57
+ "@ai-sdk/google": "4.0.50",
58
+ "@ai-sdk/openai": "4.0.46",
59
+ "@ai-sdk/xai": "4.0.43",
60
+ "ai": "7.0.77",
61
61
  "front-matter": "4.0.2",
62
62
  "zod": "4.4.3"
63
63
  },
@@ -66,7 +66,7 @@
66
66
  "@tsconfig/node-lts": "24.0.1",
67
67
  "@tsconfig/node-ts": "23.6.4",
68
68
  "@tsconfig/node24": "24.0.5",
69
- "@types/bun": "1.3.14",
69
+ "@types/bun": "1.4.0",
70
70
  "@types/node": "26.2.0",
71
71
  "@willbooster/oxfmt-config": "1.2.2",
72
72
  "@willbooster/oxlint-config": "1.4.8",
@@ -75,8 +75,8 @@
75
75
  "build-ts": "21.0.9",
76
76
  "conventional-changelog-conventionalcommits": "10.3.0",
77
77
  "lefthook": "2.1.10",
78
- "oxfmt": "0.63.0",
79
- "oxlint": "1.78.0",
78
+ "oxfmt": "0.64.0",
79
+ "oxlint": "1.79.0",
80
80
  "oxlint-tsgolint": "7.0.2001",
81
81
  "prettier": "3.9.6",
82
82
  "prettier-plugin-java": "2.10.3",
@@ -84,7 +84,7 @@
84
84
  "semantic-release": "25.0.9",
85
85
  "sort-package-json": "4.0.0",
86
86
  "typescript": "7.0.2",
87
- "vitest": "4.1.10"
87
+ "vitest": "4.1.11"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=24"