@exercode/problem-utils 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.js +1 -1
  4. package/dist/cli/checkAllProblems.js.map +1 -1
  5. package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
  6. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  7. package/dist/helpers/checkProblemDirIsolation.d.ts +9 -16
  8. package/dist/helpers/checkProblemDirIsolation.js +1 -1
  9. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  10. package/dist/helpers/runHarnessProcess.cjs +2 -0
  11. package/dist/helpers/runHarnessProcess.cjs.map +1 -0
  12. package/dist/helpers/runHarnessProcess.d.ts +23 -0
  13. package/dist/helpers/runHarnessProcess.js +2 -0
  14. package/dist/helpers/runHarnessProcess.js.map +1 -0
  15. package/dist/helpers/runIsolationCheckInDebugMode.cjs +2 -0
  16. package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -0
  17. package/dist/helpers/runIsolationCheckInDebugMode.d.ts +7 -0
  18. package/dist/helpers/runIsolationCheckInDebugMode.js +2 -0
  19. package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -0
  20. package/dist/helpers/temporaryProblemDirCopy.cjs +2 -0
  21. package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -0
  22. package/dist/helpers/temporaryProblemDirCopy.d.ts +15 -0
  23. package/dist/helpers/temporaryProblemDirCopy.js +2 -0
  24. package/dist/helpers/temporaryProblemDirCopy.js.map +1 -0
  25. package/dist/presets/command.cjs +1 -1
  26. package/dist/presets/command.cjs.map +1 -1
  27. package/dist/presets/command.js +1 -1
  28. package/dist/presets/command.js.map +1 -1
  29. package/dist/presets/evaluation.cjs +4 -4
  30. package/dist/presets/evaluation.cjs.map +1 -1
  31. package/dist/presets/evaluation.js +4 -4
  32. package/dist/presets/evaluation.js.map +1 -1
  33. package/package.json +1 -1
@@ -1 +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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"}
1
+ {"version":3,"file":"evaluation.cjs","names":["parseArgs","resolveCwds","passesIsolationCheckInDebugMode","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 { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { CsvParseError, type CsvParseErrorReason, parseCsv, parseCsvRecords } from '../helpers/parseCsv.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { isSafeSubmissionOutputPath } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { passesIsolationCheckInDebugMode } from '../helpers/runIsolationCheckInDebugMode.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 && !(await passesIsolationCheckInDebugMode(problemDir, cwds, args.params))) {\n process.exitCode = 1;\n return;\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,11 +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`| 指標 | スコア |
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{judgeByStaticAnalysis as o}from"../helpers/judgeByStaticAnalysis.js";import{isSafeSubmissionOutputPath as s}from"../helpers/readOutputFiles.js";import{passesIsolationCheckInDebugMode as c}from"../helpers/runIsolationCheckInDebugMode.js";import{readProblemMarkdownFrontMatter as l}from"../helpers/readProblemMarkdownFrontMatter.js";import{printDebugCwdBanner as u,printDebugExpectationFailureBanner as d,resolveCwds as f}from"../helpers/resolveCwds.js";import p from"node:fs/promises";import m from"node:path";const h={unterminated_quote:'引用符(`"`)が閉じられていません',unexpected_quote:'引用符(`"`)は値全体を囲む位置にしか書けません',text_after_quoted_field:`閉じ引用符の後に区切り文字以外の文字があります`},g={rmsle:{label:`RMSLE`,isHigherBetter:!1,validatePrediction:e=>k(e)??(Number(e)<0?`負の値です`:void 0),compute:(e,t)=>Math.sqrt(A(e.map((e,n)=>(Math.log1p(Number(e))-Math.log1p(Number(t[n])))**2)))},rmse:{label:`RMSE`,isHigherBetter:!1,validatePrediction:k,compute:(e,t)=>Math.sqrt(A(e.map((e,n)=>(Number(e)-Number(t[n]))**2)))},mae:{label:`MAE`,isHigherBetter:!1,validatePrediction:k,compute:(e,t)=>A(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)=>A(e.map((e,n)=>+(e.trim()===t[n]?.trim())))}};async function _(t,n){let r=e(process.argv),{cwds:a,isDebugMode:o}=await f(t,r.cwd);if(o&&!await c(t,a,r.params)){process.exitCode=1;return}let s=await C(m.join(t,n.answerFilePath),n);for(let e of a){o&&u(t,e);let r=await v(t,e.cwd,s,n);i({testCaseId:`evaluation`,...r}),o&&!O(e,r)&&(process.exitCode=1,d(t,e))}}async function v(e,t,n,r){let i=await o(t,await l(e));if(i)return i;let c=r.submissionFilePath??`submission.csv`,u;try{let e=m.join(t,c);if(!await s(t,e))throw Error(`unsafe path`);u=await p.readFile(e,`utf8`)}catch{return{decisionCode:a.MISSING_REQUIRED_SUBMISSION_FILE_ERROR,feedbackMarkdown:`\`${c}\`が見つかりません。予測結果のCSVファイルを\`${c}\`という名前で提出してください。`}}let d=S(u,c,n,r);if(typeof d==`string`)return{decisionCode:a.WRONG_ANSWER,feedbackMarkdown:d};let f=[...n.values()],h=r.metric.compute([...n.keys()].map(e=>d.get(e)),f);if(!Number.isFinite(h))return{decisionCode:a.WRONG_ANSWER,feedbackMarkdown:`予測値が極端なため、${r.metric.label}を計算できませんでした。予測値を見直してください。`};let g=r.acceptableScore===void 0||(r.metric.isHigherBetter?h>=r.acceptableScore:h<=r.acceptableScore);return{decisionCode:g?a.ACCEPTED:a.WRONG_ANSWER,score:h,scoreLabel:r.metric.label,feedbackMarkdown:y(h,f.length,g,r)}}function y(e,t,n,r){let{label:i,isHigherBetter:a}=r.metric,o=b(e,r.acceptableScore),s=r.acceptableScore===void 0?``:`\n合格基準: ${i} ${a?`≧`:`≦`} ${x(r.acceptableScore,o)}(${n?`達成`:`未達成`})`;return`| 指標 | スコア |
2
2
  | ---- | ------ |
3
- | ${i} | ${S(e,o)} |
3
+ | ${i} | ${x(e,o)} |
4
4
 
5
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}\`の内容に問題があります。
6
+ `}function b(e,t){if(t===void 0||e===t)return 6;for(let n=6;n<17;n++)if(x(e,n)!==x(t,n))return n;return 17}function x(e,t){return Number(e.toPrecision(t)).toString()}function S(e,n,i,a){let o;try{o=r(e)}catch(e){return`\`${n}\`をCSVとして読み込めませんでした。${e instanceof t?`${e.lineNumber}行目付近: ${h[e.reason]}`:``}`}let s=w(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(T(e))continue;let n=e[s.idIndex]?.trim()??``,r=e[s.targetIndex]??``;if(!i.has(n))d(`${t}行目: \`${a.idColumn}\`が\`${D(n)}\`の行は評価対象ではありません`);else if(c.has(n))d(`${t}行目: \`${a.idColumn}\`が\`${D(n)}\`の行が重複しています`);else{let e=a.metric.validatePrediction?.(r);e&&d(`${t}行目: \`${a.targetColumn}\`の値\`${D(r)}\`は${e}`),c.set(n,r)}}let f=[...i.keys()].filter(e=>!c.has(e));return f.length>0&&(u++,l.unshift(`\`${a.idColumn}\`が${E(f)}の行がありません(不足 ${f.length.toLocaleString(`en-US`)}件)`)),u>0?`\`${n}\`の内容に問題があります。
7
7
 
8
8
  ${l.map(e=>`- ${e}`).join(`
9
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};
10
+ `:c}async function C(e,t){let r=n(await p.readFile(e,`utf8`)),i=w(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(T(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 w(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 T(e){return e.every(e=>e.trim()===``)}function E(e){let t=e.slice(0,10).map(e=>`\`${D(e)}\``);return e.length>10?`${t.join(`, `)}, ...`:t.join(`, `)}function D(e){let t=e.replaceAll(/[\p{Cc}]/gu,` `).replaceAll("`",`'`);return t.trim()===``?`(空)`:t.length>50?`${t.slice(0,50)}…`:t}function O(e,t){return t.decisionCode===a.ACCEPTED==(e.expectedResult===`accepted`)}function k(e){return Number.isFinite(Number(e))&&e.trim()!==``?void 0:`数値ではありません`}function A(e){return e.reduce((e,t)=>e+t,0)/e.length}export{_ as evaluationJudgePreset,g as evaluationMetrics};
11
11
  //# sourceMappingURL=evaluation.js.map
@@ -1 +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
+ {"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 { judgeByStaticAnalysis } from '../helpers/judgeByStaticAnalysis.js';\nimport { parseArgs } from '../helpers/parseArgs.js';\nimport { CsvParseError, type CsvParseErrorReason, parseCsv, parseCsvRecords } from '../helpers/parseCsv.js';\nimport { printTestCaseResult } from '../helpers/printTestCaseResult.js';\nimport { isSafeSubmissionOutputPath } from '../helpers/readOutputFiles.js';\nimport { readProblemMarkdownFrontMatter } from '../helpers/readProblemMarkdownFrontMatter.js';\nimport { passesIsolationCheckInDebugMode } from '../helpers/runIsolationCheckInDebugMode.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 && !(await passesIsolationCheckInDebugMode(problemDir, cwds, args.params))) {\n process.exitCode = 1;\n return;\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exercode/problem-utils",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": ":100: A set of utilities for judging programs on Exercode (https://exercode.willbooster.com/).",
5
5
  "keywords": [
6
6
  "exercode",