#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import process from "node:process"; const DEFAULT_MANIFEST_PATH = "docs/test/harness-evals.manifest.json"; const REVIEW_DECISION_JSON_CANDIDATES = [ "review-decision.json", "../review/review-decision.json", ]; const REVIEW_DECISION_STATUS_SET = new Set([ "accepted", "deferred", "rejected", "needs_more_evidence", "pending_review", ]); const REVIEW_DECISION_RISK_LEVEL_SET = new Set([ "low", "medium", "high", "unknown", ]); function parseArgs(argv) { const result = { format: "text", help: false, historyRetain: 30, manifest: DEFAULT_MANIFEST_PATH, outputJson: "", outputMarkdown: "", recordHistoryDir: "", strict: true, workspaceRoot: process.cwd(), }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--manifest" && argv[index + 1]) { result.manifest = String(argv[index + 1]).trim(); index += 1; continue; } if (arg === "--workspace-root" && argv[index + 1]) { result.workspaceRoot = String(argv[index + 1]).trim(); index += 1; continue; } if (arg === "--format" && argv[index + 1]) { result.format = String(argv[index + 1]).trim(); index += 1; continue; } if (arg === "--output-json" && argv[index + 1]) { result.outputJson = String(argv[index + 1]).trim(); index += 1; continue; } if (arg === "--output-markdown" && argv[index + 1]) { result.outputMarkdown = String(argv[index + 1]).trim(); index += 1; continue; } if (arg === "--record-history-dir" && argv[index + 1]) { result.recordHistoryDir = String(argv[index + 1]).trim(); index += 1; continue; } if (arg === "--history-retain" && argv[index + 1]) { result.historyRetain = Number.parseInt(String(argv[index + 1]), 10); index += 1; continue; } if (arg === "--no-strict") { result.strict = false; continue; } if (arg === "--strict") { result.strict = true; continue; } if (arg === "--help" || arg === "-h") { result.help = true; } } return result; } function printHelp() { console.log(` Lime Harness Eval Runner 用法: node scripts/harness-eval-runner.mjs node scripts/harness-eval-runner.mjs --format json node scripts/harness-eval-runner.mjs --workspace-root "/path/to/workspace" node scripts/harness-eval-runner.mjs --output-json "./tmp/harness-eval-summary.json" --output-markdown "./tmp/harness-eval-summary.md" node scripts/harness-eval-runner.mjs --record-history-dir "./artifacts/history" 选项: --manifest PATH 指定 manifest,默认 docs/test/harness-evals.manifest.json --workspace-root PATH 指定工作区根目录,默认当前目录 --format FMT 控制标准输出格式:text | json | markdown --output-json PATH 将 JSON 摘要写入指定路径 --output-markdown PATH 将 Markdown 摘要写入指定路径 --record-history-dir PATH 将当前 summary 追加写入历史目录,供 trend/nightly 复用 --history-retain N 历史目录最多保留多少条 summary,默认 30 --strict 严格模式(默认),发现 invalid case 时返回非 0 --no-strict 非严格模式,只输出摘要,不因 invalid case 退出失败 -h, --help 显示帮助 `); } function readJsonFile(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")); } function resolvePath(baseDir, relativePath) { return path.resolve(baseDir, relativePath); } function ensureParentDirectory(filePath) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } function ensureDirectory(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function toHistoryTimestamp(generatedAt) { const parsed = new Date(generatedAt); const normalized = Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString(); return normalized.replace(/[-:]/g, "").replace(/\.(\d{3})Z$/, "$1Z"); } function trimHistoryDirectory(historyDir, retainCount) { const normalizedRetainCount = Number.isInteger(retainCount) && retainCount > 0 ? retainCount : 30; const historyFiles = fs .readdirSync(historyDir, { withFileTypes: true }) .filter( (entry) => entry.isFile() && entry.name.endsWith("-harness-eval-summary.json"), ) .map((entry) => path.join(historyDir, entry.name)) .sort((left, right) => right.localeCompare(left)); for (const staleFile of historyFiles.slice(normalizedRetainCount)) { fs.rmSync(staleFile, { force: true }); } } function recordSummaryHistory(repoRoot, summary, options) { if (!options.recordHistoryDir) { return ""; } const historyDir = resolvePath(repoRoot, options.recordHistoryDir); ensureDirectory(historyDir); const historyFilePath = path.join( historyDir, `${toHistoryTimestamp(summary.generatedAt)}-harness-eval-summary.json`, ); fs.writeFileSync( historyFilePath, `${JSON.stringify(summary, null, 2)}\n`, "utf8", ); trimHistoryDirectory(historyDir, options.historyRetain); return historyFilePath; } function normalizeStringList(value) { if (!Array.isArray(value)) { return []; } return value .map((item) => (typeof item === "string" ? item.trim() : "")) .filter(Boolean); } function mergeUniqueStrings(...groups) { return [...new Set(groups.flatMap((group) => normalizeStringList(group)))]; } function normalizeOptionalString(value) { return typeof value === "string" && value.trim().length > 0 ? value.trim() : ""; } function normalizeEnumString(value, allowedValues, fallback = "") { const normalized = normalizeOptionalString(value); if (allowedValues.has(normalized)) { return normalized; } return fallback; } function createBreakdownEntry(name) { return { name, caseCount: 0, readyCount: 0, invalidCount: 0, pendingRequestCaseCount: 0, needsHumanReviewCount: 0, }; } function aggregateCaseBreakdown(cases, selector) { const breakdownMap = new Map(); for (const entry of cases) { const labels = mergeUniqueStrings(selector(entry)); for (const label of labels) { const current = breakdownMap.get(label) ?? createBreakdownEntry(label); current.caseCount += 1; if (entry.status === "ready") { current.readyCount += 1; } else if (entry.status === "invalid") { current.invalidCount += 1; } if (entry.pendingRequestCount > 0) { current.pendingRequestCaseCount += 1; } if (entry.requiresHumanReview) { current.needsHumanReviewCount += 1; } breakdownMap.set(label, current); } } return Array.from(breakdownMap.values()).sort((left, right) => { if (right.caseCount !== left.caseCount) { return right.caseCount - left.caseCount; } return left.name.localeCompare(right.name); }); } function getValueByPath(target, dottedPath) { return dottedPath .split(".") .reduce( (current, segment) => (current == null ? undefined : current[segment]), target, ); } function isPresentValue(value) { if (value == null) { return false; } if (typeof value === "string") { return value.trim().length > 0; } if (Array.isArray(value)) { return value.length > 0; } return true; } function collectFieldIssues(jsonPayload, fields, label) { const issues = []; for (const field of fields) { if (!isPresentValue(getValueByPath(jsonPayload, field))) { issues.push(`${label} 缺少字段: ${field}`); } } return issues; } function listReplayDirectories(rootPath) { if (!fs.existsSync(rootPath)) { return []; } const entries = fs.readdirSync(rootPath, { withFileTypes: true }); return entries .filter((entry) => entry.isDirectory()) .map((entry) => path.join(rootPath, entry.name, "replay")) .filter((replayPath) => { try { return fs.statSync(replayPath).isDirectory(); } catch { return false; } }) .sort((left, right) => left.localeCompare(right)); } function resolveExistingReviewDecisionPath(caseDir) { for (const relativePath of REVIEW_DECISION_JSON_CANDIDATES) { const candidate = path.resolve(caseDir, relativePath); if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { return candidate; } } return ""; } function normalizeReviewDecisionPayload(payload) { const decision = payload && typeof payload === "object" && payload.decision ? payload.decision : {}; return { decisionStatus: normalizeEnumString( decision.decisionStatus ?? decision.decision_status ?? payload?.decisionStatus ?? payload?.decision_status, REVIEW_DECISION_STATUS_SET, "pending_review", ), riskLevel: normalizeEnumString( decision.riskLevel ?? decision.risk_level ?? payload?.riskLevel ?? payload?.risk_level, REVIEW_DECISION_RISK_LEVEL_SET, "unknown", ), humanReviewer: normalizeOptionalString( decision.humanReviewer ?? decision.human_reviewer ?? payload?.humanReviewer ?? payload?.human_reviewer, ), reviewedAt: normalizeOptionalString( decision.reviewedAt ?? decision.reviewed_at ?? payload?.reviewedAt ?? payload?.reviewed_at, ), }; } function loadReviewDecisionForCase(caseDir, inlineReviewDecision) { const reviewDecisionPath = resolveExistingReviewDecisionPath(caseDir); if (reviewDecisionPath) { try { return { reviewDecision: normalizeReviewDecisionPayload( readJsonFile(reviewDecisionPath), ), issues: [], }; } catch (error) { return { reviewDecision: null, issues: [ `review-decision.json 解析失败: ${String(error.message ?? error)}`, ], }; } } if (inlineReviewDecision && typeof inlineReviewDecision === "object") { return { reviewDecision: normalizeReviewDecisionPayload(inlineReviewDecision), issues: [], }; } return { reviewDecision: null, issues: [], }; } function normalizeObservabilityCoverage(inputPayload, evidencePayload) { const inlineSummary = inputPayload?.observability && typeof inputPayload.observability === "object" && !Array.isArray(inputPayload.observability) ? inputPayload.observability : evidencePayload?.observabilitySummary && typeof evidencePayload.observabilitySummary === "object" && !Array.isArray(evidencePayload.observabilitySummary) ? evidencePayload.observabilitySummary : null; if (!inlineSummary) { return [ { signal: "observabilitySummary", status: "missing", }, ]; } const signalCoverage = Array.isArray(inlineSummary.signalCoverage) ? inlineSummary.signalCoverage : []; if (signalCoverage.length === 0) { return [ { signal: "observabilitySummary", status: "missing_signal_coverage", }, ]; } const seen = new Set(); const coverage = []; for (const entry of signalCoverage) { if (!entry || typeof entry !== "object") { continue; } const signal = normalizeOptionalString(entry.signal ?? entry.name); const status = normalizeOptionalString(entry.status); if (!signal || !status) { continue; } const fingerprint = `${signal}:${status}`; if (seen.has(fingerprint)) { continue; } seen.add(fingerprint); coverage.push({ signal, status, source: normalizeOptionalString(entry.source), detail: normalizeOptionalString(entry.detail), }); } if (coverage.length === 0) { return [ { signal: "observabilitySummary", status: "missing_signal_coverage", }, ]; } return coverage; } function validateCaseDirectory(caseDir, caseConfig, defaults, context) { const requiredArtifacts = normalizeStringList( caseConfig.requiredArtifacts ?? defaults.requiredArtifacts, ); const requiredInputFields = normalizeStringList( caseConfig.requiredInputFields ?? defaults.requiredInputFields, ); const requiredExpectedFields = normalizeStringList( caseConfig.requiredExpectedFields ?? defaults.requiredExpectedFields, ); const requiredEvidenceFields = normalizeStringList( caseConfig.requiredEvidenceFields ?? defaults.requiredEvidenceFields, ); const issues = []; const resolvedCaseDir = path.resolve(caseDir); const files = {}; for (const artifactName of requiredArtifacts) { const artifactPath = path.join(resolvedCaseDir, artifactName); files[artifactName] = artifactPath; if (!fs.existsSync(artifactPath)) { issues.push(`缺少文件: ${artifactName}`); } } let inputPayload = null; let expectedPayload = null; let evidencePayload = null; let reviewDecision = null; if (fs.existsSync(files["input.json"] ?? "")) { try { inputPayload = readJsonFile(files["input.json"]); issues.push( ...collectFieldIssues(inputPayload, requiredInputFields, "input.json"), ); } catch (error) { issues.push(`input.json 解析失败: ${String(error.message ?? error)}`); } } if (fs.existsSync(files["expected.json"] ?? "")) { try { expectedPayload = readJsonFile(files["expected.json"]); issues.push( ...collectFieldIssues( expectedPayload, requiredExpectedFields, "expected.json", ), ); } catch (error) { issues.push(`expected.json 解析失败: ${String(error.message ?? error)}`); } } if (fs.existsSync(files["evidence-links.json"] ?? "")) { try { evidencePayload = readJsonFile(files["evidence-links.json"]); issues.push( ...collectFieldIssues( evidencePayload, requiredEvidenceFields, "evidence-links.json", ), ); } catch (error) { issues.push( `evidence-links.json 解析失败: ${String(error.message ?? error)}`, ); } } const reviewDecisionResult = loadReviewDecisionForCase( resolvedCaseDir, caseConfig.reviewDecision, ); reviewDecision = reviewDecisionResult.reviewDecision; issues.push(...reviewDecisionResult.issues); const observabilityCoverage = normalizeObservabilityCoverage( inputPayload, evidencePayload, ); const observabilitySignals = observabilityCoverage.map( (entry) => `${entry.signal}:${entry.status}`, ); const observabilityGapCount = observabilityCoverage.filter( (entry) => entry.status !== "exported", ).length; const pendingRequestCount = Array.isArray( inputPayload?.runtimeContext?.pendingRequests, ) ? inputPayload.runtimeContext.pendingRequests.length : 0; const classificationTags = mergeUniqueStrings( context.tags, inputPayload?.classification?.suiteTags, ); const failureModes = normalizeStringList( inputPayload?.classification?.failureModes, ); const primaryBlockingKind = typeof inputPayload?.classification?.primaryBlockingKind === "string" ? inputPayload.classification.primaryBlockingKind.trim() : ""; const requiresHumanReview = expectedPayload?.graderSuggestion?.requiresHumanReview === true; const preferredMode = typeof expectedPayload?.graderSuggestion?.preferredMode === "string" ? expectedPayload.graderSuggestion.preferredMode : ""; const reviewDecisionRecorded = reviewDecision != null; return { caseId: context.caseId, title: context.title, suiteId: context.suiteId, suiteTitle: context.suiteTitle, source: context.source, priority: context.priority ?? "", tags: classificationTags, failureModes, primaryBlockingKind, caseDir: resolvedCaseDir, relativeCaseDir: path.relative(context.repoRoot, resolvedCaseDir) || ".", sessionId: inputPayload?.session?.sessionId ?? expectedPayload?.sessionId ?? path.basename(path.dirname(resolvedCaseDir)), threadId: inputPayload?.session?.threadId ?? expectedPayload?.threadId ?? "", goalSummary: inputPayload?.task?.goalSummary ?? expectedPayload?.goalSummary ?? "", pendingRequestCount, requiresHumanReview, reviewDecisionRecorded, reviewDecisionStatus: reviewDecision?.decisionStatus ?? "", reviewRiskLevel: reviewDecision?.riskLevel ?? "", reviewHumanReviewer: reviewDecision?.humanReviewer ?? "", reviewReviewedAt: reviewDecision?.reviewedAt ?? "", observabilityCoverage, observabilitySignals, observabilityGapCount, preferredMode, status: issues.length === 0 ? "ready" : "invalid", issues, }; } function expandSuiteCases(suiteConfig, defaults, repoRoot, workspaceRoot) { const suiteCases = []; const configuredCases = Array.isArray(suiteConfig.cases) ? suiteConfig.cases : []; for (const caseConfig of configuredCases) { const source = String(caseConfig.source ?? "").trim(); if (source === "repo_fixture") { const caseDir = resolvePath(repoRoot, String(caseConfig.caseDir ?? "")); suiteCases.push( validateCaseDirectory(caseDir, caseConfig, defaults, { caseId: String(caseConfig.id ?? "unnamed-case"), priority: suiteConfig.priority, repoRoot, source, suiteId: String(suiteConfig.id ?? "unnamed-suite"), suiteTitle: String(suiteConfig.title ?? "未命名 Suite"), tags: caseConfig.tags, title: String(caseConfig.title ?? caseConfig.id ?? "未命名 Case"), }), ); continue; } if (source === "workspace_replay_discovery") { const discoveryRoot = resolvePath( workspaceRoot, String(caseConfig.root ?? ".lime/harness/sessions"), ); const replayDirectories = listReplayDirectories(discoveryRoot); if ( replayDirectories.length === 0 && caseConfig.allowZeroMatches !== true ) { suiteCases.push({ caseId: String(caseConfig.id ?? "workspace-discovery"), title: String(caseConfig.title ?? "工作区 Replay 自动发现"), suiteId: String(suiteConfig.id ?? "unnamed-suite"), suiteTitle: String(suiteConfig.title ?? "未命名 Suite"), source, priority: suiteConfig.priority ?? "", tags: normalizeStringList(caseConfig.tags), failureModes: [], primaryBlockingKind: "", caseDir: discoveryRoot, relativeCaseDir: path.relative(repoRoot, discoveryRoot) || ".", sessionId: "", threadId: "", goalSummary: "", pendingRequestCount: 0, requiresHumanReview: false, reviewDecisionRecorded: false, reviewDecisionStatus: "", reviewRiskLevel: "", reviewHumanReviewer: "", reviewReviewedAt: "", observabilityCoverage: [ { signal: "observabilitySummary", status: "missing" }, ], observabilitySignals: ["observabilitySummary:missing"], observabilityGapCount: 1, preferredMode: "", status: "invalid", issues: [ `未发现 replay case 目录: ${path.relative(workspaceRoot, discoveryRoot) || "."}`, ], }); continue; } for (const replayDir of replayDirectories) { const sessionId = path.basename(path.dirname(replayDir)); suiteCases.push( validateCaseDirectory(replayDir, caseConfig, defaults, { caseId: `${String(caseConfig.id ?? "workspace-case")}:${sessionId}`, priority: suiteConfig.priority, repoRoot, source, suiteId: String(suiteConfig.id ?? "unnamed-suite"), suiteTitle: String(suiteConfig.title ?? "未命名 Suite"), tags: caseConfig.tags, title: `${String(caseConfig.title ?? "工作区 Replay 样本")} / ${sessionId}`, }), ); } continue; } suiteCases.push({ caseId: String(caseConfig.id ?? "unknown-case"), title: String(caseConfig.title ?? "未命名 Case"), suiteId: String(suiteConfig.id ?? "unnamed-suite"), suiteTitle: String(suiteConfig.title ?? "未命名 Suite"), source, priority: suiteConfig.priority ?? "", tags: normalizeStringList(caseConfig.tags), failureModes: [], primaryBlockingKind: "", caseDir: "", relativeCaseDir: "", sessionId: "", threadId: "", goalSummary: "", pendingRequestCount: 0, requiresHumanReview: false, reviewDecisionRecorded: false, reviewDecisionStatus: "", reviewRiskLevel: "", reviewHumanReviewer: "", reviewReviewedAt: "", observabilityCoverage: [{ signal: "observabilitySummary", status: "missing" }], observabilitySignals: ["observabilitySummary:missing"], observabilityGapCount: 1, preferredMode: "", status: "invalid", issues: [`不支持的 case source: ${source || "(empty)"}`], }); } const readyCount = suiteCases.filter( (entry) => entry.status === "ready", ).length; const invalidCount = suiteCases.length - readyCount; const discoveredCount = suiteCases.filter( (entry) => entry.source === "workspace_replay_discovery", ).length; return { id: String(suiteConfig.id ?? "unnamed-suite"), title: String(suiteConfig.title ?? "未命名 Suite"), priority: String(suiteConfig.priority ?? ""), roadmap: String(suiteConfig.roadmap ?? ""), description: String(suiteConfig.description ?? ""), upstream: suiteConfig.upstream ?? {}, cases: suiteCases, stats: { configuredCaseCount: configuredCases.length, discoveredCaseCount: discoveredCount, caseCount: suiteCases.length, readyCount, invalidCount, }, }; } function buildSummary(manifest, suites, options) { const allCases = suites.flatMap((suite) => suite.cases); const readyCases = allCases.filter((entry) => entry.status === "ready"); const invalidCases = allCases.filter((entry) => entry.status === "invalid"); const reviewCases = allCases.filter((entry) => entry.requiresHumanReview); const pendingCases = allCases.filter( (entry) => entry.pendingRequestCount > 0, ); const recordedReviewDecisionCases = allCases.filter( (entry) => entry.reviewDecisionRecorded, ); const observabilityGapCases = allCases.filter( (entry) => entry.observabilityGapCount > 0, ); return { manifestVersion: String(manifest.manifestVersion ?? "unknown"), title: String(manifest.title ?? "Lime Harness Eval Summary"), generatedAt: new Date().toISOString(), repoRoot: process.cwd(), workspaceRoot: path.resolve(options.workspaceRoot), strict: options.strict, totals: { suiteCount: suites.length, caseCount: allCases.length, readyCount: readyCases.length, invalidCount: invalidCases.length, needsHumanReviewCount: reviewCases.length, pendingRequestCaseCount: pendingCases.length, reviewDecisionRecordedCount: recordedReviewDecisionCases.length, observabilityGapCaseCount: observabilityGapCases.length, }, breakdowns: { suiteTags: aggregateCaseBreakdown(allCases, (entry) => entry.tags), failureModes: aggregateCaseBreakdown( allCases, (entry) => entry.failureModes, ), reviewDecisionStatuses: aggregateCaseBreakdown(allCases, (entry) => entry.reviewDecisionStatus ? [entry.reviewDecisionStatus] : [], ), reviewRiskLevels: aggregateCaseBreakdown(allCases, (entry) => entry.reviewRiskLevel ? [entry.reviewRiskLevel] : [], ), observabilitySignals: aggregateCaseBreakdown( allCases, (entry) => entry.observabilitySignals, ), }, suites, }; } function renderText(summary) { const lines = [ `[harness-eval] manifest: ${summary.title} (${summary.manifestVersion})`, `[harness-eval] workspace: ${summary.workspaceRoot}`, `[harness-eval] suites: ${summary.totals.suiteCount}`, `[harness-eval] cases : ${summary.totals.caseCount}`, `[harness-eval] ready : ${summary.totals.readyCount}`, `[harness-eval] invalid: ${summary.totals.invalidCount}`, `[harness-eval] pending-request cases: ${summary.totals.pendingRequestCaseCount}`, `[harness-eval] needs-review cases : ${summary.totals.needsHumanReviewCount}`, `[harness-eval] recorded review decisions: ${summary.totals.reviewDecisionRecordedCount}`, `[harness-eval] observability-gap cases: ${summary.totals.observabilityGapCaseCount}`, ]; const topFailureModes = summary.breakdowns.failureModes.slice(0, 5); if (topFailureModes.length > 0) { lines.push("[harness-eval] top failure modes:"); for (const entry of topFailureModes) { lines.push( ` - ${entry.name}: case=${entry.caseCount}, invalid=${entry.invalidCount}, pending=${entry.pendingRequestCaseCount}`, ); } } const topSuiteTags = summary.breakdowns.suiteTags.slice(0, 5); if (topSuiteTags.length > 0) { lines.push("[harness-eval] top suite tags:"); for (const entry of topSuiteTags) { lines.push( ` - ${entry.name}: case=${entry.caseCount}, ready=${entry.readyCount}, invalid=${entry.invalidCount}`, ); } } const topReviewStatuses = summary.breakdowns.reviewDecisionStatuses.slice(0, 5); if (topReviewStatuses.length > 0) { lines.push("[harness-eval] review decision statuses:"); for (const entry of topReviewStatuses) { lines.push( ` - ${entry.name}: case=${entry.caseCount}, ready=${entry.readyCount}, invalid=${entry.invalidCount}`, ); } } const topObservabilitySignals = summary.breakdowns.observabilitySignals.slice( 0, 5, ); if (topObservabilitySignals.length > 0) { lines.push("[harness-eval] observability signals:"); for (const entry of topObservabilitySignals) { lines.push( ` - ${entry.name}: case=${entry.caseCount}, ready=${entry.readyCount}, invalid=${entry.invalidCount}`, ); } } for (const suite of summary.suites) { lines.push( `[harness-eval] suite ${suite.id}: ready ${suite.stats.readyCount} / ${suite.stats.caseCount}`, ); for (const entry of suite.cases) { lines.push( ` - ${entry.caseId} [${entry.status}] (${entry.source}) ${entry.relativeCaseDir}`, ); if (entry.tags.length > 0) { lines.push(` tags: ${entry.tags.join(", ")}`); } if (entry.failureModes.length > 0) { lines.push(` failure_modes: ${entry.failureModes.join(", ")}`); } if (entry.reviewDecisionStatus) { lines.push( ` review: ${entry.reviewDecisionStatus} / risk=${entry.reviewRiskLevel || "unknown"}`, ); } if (entry.observabilitySignals.length > 0) { lines.push( ` observability: ${entry.observabilitySignals.join(", ")}`, ); } for (const issue of entry.issues) { lines.push(` * ${issue}`); } } } return `${lines.join("\n")}\n`; } function renderMarkdown(summary) { const lines = [ "# Lime Harness Eval Summary", "", `- 生成时间:${summary.generatedAt}`, `- manifest:${summary.title} (${summary.manifestVersion})`, `- 工作区:\`${summary.workspaceRoot}\``, `- suite 数:${summary.totals.suiteCount}`, `- case 数:${summary.totals.caseCount}`, `- ready:${summary.totals.readyCount}`, `- invalid:${summary.totals.invalidCount}`, `- pending request case:${summary.totals.pendingRequestCaseCount}`, `- needs review case:${summary.totals.needsHumanReviewCount}`, `- 已记录人工审核:${summary.totals.reviewDecisionRecordedCount}`, `- observability gap case:${summary.totals.observabilityGapCaseCount}`, "", ]; if (summary.breakdowns.failureModes.length > 0) { lines.push("## Failure Mode 分布"); lines.push(""); lines.push( "| Failure Mode | case | invalid | pending_request | needs_review |", ); lines.push("| --- | --- | --- | --- | --- |"); for (const entry of summary.breakdowns.failureModes) { lines.push( `| ${entry.name} | ${entry.caseCount} | ${entry.invalidCount} | ${entry.pendingRequestCaseCount} | ${entry.needsHumanReviewCount} |`, ); } lines.push(""); } if (summary.breakdowns.suiteTags.length > 0) { lines.push("## Suite Tag 分布"); lines.push(""); lines.push("| Suite Tag | case | ready | invalid |"); lines.push("| --- | --- | --- | --- |"); for (const entry of summary.breakdowns.suiteTags) { lines.push( `| ${entry.name} | ${entry.caseCount} | ${entry.readyCount} | ${entry.invalidCount} |`, ); } lines.push(""); } if (summary.breakdowns.reviewDecisionStatuses.length > 0) { lines.push("## 人工审核状态分布"); lines.push(""); lines.push("| 审核状态 | case | ready | invalid |"); lines.push("| --- | --- | --- | --- |"); for (const entry of summary.breakdowns.reviewDecisionStatuses) { lines.push( `| ${entry.name} | ${entry.caseCount} | ${entry.readyCount} | ${entry.invalidCount} |`, ); } lines.push(""); } if (summary.breakdowns.reviewRiskLevels.length > 0) { lines.push("## 风险等级分布"); lines.push(""); lines.push("| 风险等级 | case | ready | invalid |"); lines.push("| --- | --- | --- | --- |"); for (const entry of summary.breakdowns.reviewRiskLevels) { lines.push( `| ${entry.name} | ${entry.caseCount} | ${entry.readyCount} | ${entry.invalidCount} |`, ); } lines.push(""); } if (summary.breakdowns.observabilitySignals.length > 0) { lines.push("## Observability Coverage 分布"); lines.push(""); lines.push("| Signal | case | ready | invalid |"); lines.push("| --- | --- | --- | --- |"); for (const entry of summary.breakdowns.observabilitySignals) { lines.push( `| ${entry.name} | ${entry.caseCount} | ${entry.readyCount} | ${entry.invalidCount} |`, ); } lines.push(""); } for (const suite of summary.suites) { lines.push(`## ${suite.title}`); lines.push(""); if (suite.description) { lines.push(suite.description); lines.push(""); } lines.push(`- ` + `suite_id:\`${suite.id}\``); if (suite.priority) { lines.push(`- 优先级:${suite.priority}`); } if (suite.roadmap) { lines.push(`- 路线图:${suite.roadmap}`); } lines.push( `- ready / total:${suite.stats.readyCount} / ${suite.stats.caseCount}`, ); lines.push(""); lines.push("| Case | 状态 | 来源 | 分类 | 审核 | 目录 | 问题 |"); lines.push("| --- | --- | --- | --- | --- | --- | --- |"); for (const entry of suite.cases) { const issueText = entry.issues.length === 0 ? "无" : entry.issues.join("
"); const classificationText = []; if (entry.tags.length > 0) { classificationText.push(`tags: ${entry.tags.join(", ")}`); } if (entry.failureModes.length > 0) { classificationText.push(`failure: ${entry.failureModes.join(", ")}`); } if (entry.primaryBlockingKind) { classificationText.push(`blocking: ${entry.primaryBlockingKind}`); } if (entry.observabilitySignals.length > 0) { classificationText.push( `observability: ${entry.observabilitySignals.join(", ")}`, ); } const reviewText = entry.reviewDecisionStatus ? [ entry.reviewDecisionStatus, entry.reviewRiskLevel ? `risk: ${entry.reviewRiskLevel}` : "", entry.reviewHumanReviewer ? `by: ${entry.reviewHumanReviewer}` : "", ] .filter(Boolean) .join("
") : "无"; lines.push( `| ${entry.caseId} | ${entry.status} | ${entry.source} | ${classificationText.join("
") || "无"} | ${reviewText} | \`${entry.relativeCaseDir || "."}\` | ${issueText} |`, ); } lines.push(""); } return `${lines.join("\n")}\n`; } function determineExitCode(summary, options) { if (!options.strict) { return 0; } return summary.totals.invalidCount > 0 ? 1 : 0; } function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { printHelp(); return; } const repoRoot = process.cwd(); const manifestPath = resolvePath(repoRoot, options.manifest); const manifest = readJsonFile(manifestPath); const defaults = manifest.defaults ?? {}; const suiteConfigs = Array.isArray(manifest.suites) ? manifest.suites : []; const suites = suiteConfigs.map((suiteConfig) => expandSuiteCases( suiteConfig, defaults, repoRoot, path.resolve(options.workspaceRoot), ), ); const summary = buildSummary(manifest, suites, options); const jsonOutput = `${JSON.stringify(summary, null, 2)}\n`; const markdownOutput = renderMarkdown(summary); const textOutput = renderText(summary); const historyFilePath = recordSummaryHistory(repoRoot, summary, options); if (options.outputJson) { const outputPath = resolvePath(repoRoot, options.outputJson); ensureParentDirectory(outputPath); fs.writeFileSync(outputPath, jsonOutput, "utf8"); } if (options.outputMarkdown) { const outputPath = resolvePath(repoRoot, options.outputMarkdown); ensureParentDirectory(outputPath); fs.writeFileSync(outputPath, markdownOutput, "utf8"); } if (options.format === "json") { process.stdout.write(jsonOutput); } else if (options.format === "markdown") { process.stdout.write(markdownOutput); } else { process.stdout.write(textOutput); if (historyFilePath) { process.stdout.write( `[harness-eval] history snapshot: ${historyFilePath}\n`, ); } } const exitCode = determineExitCode(summary, options); if (exitCode !== 0) { process.exit(exitCode); } } main();