#!/usr/bin/env node // 기존 실행 기록의 정적 재판정. 모델을 부르지 않는다 — run.mjs 가 남긴 원본 스트림(raw.ndjson·raw.jsonl)을 // 다시 읽어 (1) 기본 판정, (2) 경로 판정(V-36), (3) 읽기 비용(V-40)을 낸다. // // node build/eval/interview/rejudge.mjs --label redteam-2026-10-02[,다른-라벨] // [--runs-root outputs/eval/interview/runs] [--scenarios S4,S6] [--out <표.md>] [--json <상세.json>] // // --label 은 콤마로 여럿 줄 수 있다. 결과는 표준 출력으로 내고, --out·--json 을 주면 그 파일에도 쓴다 // (저장소 안에 쓰지 않도록 outputs/ 아래나 임시 경로를 준다). import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { judge, normalizeClaude, normalizeCodex } from "./lib/judge.mjs"; import { findCanaryFiles } from "./lib/fsutil.mjs"; import { staticJudgements, findSkillCopy } from "./lib/static.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(HERE, "..", "..", ".."); function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { if (!argv[i].startsWith("--")) continue; const next = argv[i + 1]; if (next === undefined || next.startsWith("--")) out[argv[i].slice(2)] = true; else out[argv[i].slice(2)] = argv[++i]; } return out; } /** * 실행 디렉터리 하나를 재판정한다. 원본 스트림이 없으면 null. * @returns {{scenario, runner, run, verdict, note, route, reads, readsVerdict, readsLine}|null} */ export function rejudgeRun(scenario, runner, runDir) { const isCodex = runner === "codex"; const rawPath = path.join(runDir, isCodex ? "raw.jsonl" : "raw.ndjson"); if (!fs.existsSync(rawPath)) return null; const events = (isCodex ? normalizeCodex : normalizeClaude)(fs.readFileSync(rawPath, "utf8")); const extra = {}; if (scenario.injection?.canary) { extra.workdirCanaryHits = findCanaryFiles(path.join(runDir, "work"), scenario.injection.canary, Object.keys(scenario.seedFiles || {})); } const base = judge(scenario, events, extra); const st = staticJudgements(scenario, events, { skillDir: findSkillCopy(runDir) }); return { scenario: scenario.id, runner, run: path.basename(runDir), verdict: base.verdict, note: base.note, ...st }; } /** 라벨 하나(runs/