Files
proxycast/scripts/harness-eval-runner.mjs
T
2026-04-13 13:07:08 +08:00

1338 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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",
]);
const OBSERVABILITY_GAP_SUITE_TAG = "observability-gap";
const RECOVERED_VERIFICATION_OUTCOMES = new Set([
"artifactValidator:repaired",
"browserVerification:success",
"guiSmoke:passed",
"guiSmoke:clean",
]);
function parseArgs(argv) {
const result = {
format: "text",
help: false,
manifest: DEFAULT_MANIFEST_PATH,
outputJson: "",
outputMarkdown: "",
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 === "--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"
选项:
--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 摘要写入指定路径
--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 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 isObject(value) {
return value != null && typeof value === "object" && !Array.isArray(value);
}
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 collectRecoveredVerificationOutcomes(outcomes) {
return mergeUniqueStrings(outcomes).filter((entry) =>
RECOVERED_VERIFICATION_OUTCOMES.has(entry),
);
}
function isDegradedObservabilityGapCase(entry) {
return (
entry.observabilityGapCount > 0 &&
normalizeStringList(entry.tags).includes(OBSERVABILITY_GAP_SUITE_TAG)
);
}
function isCurrentObservabilityGapCase(entry) {
return entry.observabilityGapCount > 0 && !isDegradedObservabilityGapCase(entry);
}
function isDegradedObservabilityDiagnosticCase(entry) {
return normalizeStringList(entry?.tags).includes(OBSERVABILITY_GAP_SUITE_TAG);
}
function isCurrentObservabilityDiagnosticCase(entry) {
return !isDegradedObservabilityDiagnosticCase(entry);
}
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 resolveObservabilitySummary(inputPayload, evidencePayload) {
const inlineSummary = isObject(inputPayload?.observability)
? inputPayload.observability
: null;
const evidenceSummary = isObject(evidencePayload?.observabilitySummary)
? evidencePayload.observabilitySummary
: null;
if (!inlineSummary && !evidenceSummary) {
return null;
}
return {
signalCoverage: Array.isArray(inlineSummary?.signalCoverage)
? inlineSummary.signalCoverage
: Array.isArray(evidenceSummary?.signalCoverage)
? evidenceSummary.signalCoverage
: [],
verificationSummary: isObject(inlineSummary?.verificationSummary)
? inlineSummary.verificationSummary
: isObject(evidenceSummary?.verificationSummary)
? evidenceSummary.verificationSummary
: null,
};
}
function normalizeObservabilityCoverage(inputPayload, evidencePayload) {
const observabilitySummary = resolveObservabilitySummary(
inputPayload,
evidencePayload,
);
if (!observabilitySummary) {
return [
{
signal: "observabilitySummary",
status: "missing",
},
];
}
const signalCoverage = Array.isArray(observabilitySummary.signalCoverage)
? observabilitySummary.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 pushUniqueObservabilityVerificationOutcome(outcomes, seen, signal, outcome) {
const normalizedSignal = normalizeOptionalString(signal);
const normalizedOutcome = normalizeOptionalString(outcome);
if (!normalizedSignal || !normalizedOutcome) {
return;
}
const fingerprint = `${normalizedSignal}:${normalizedOutcome}`;
if (seen.has(fingerprint)) {
return;
}
seen.add(fingerprint);
outcomes.push({
signal: normalizedSignal,
outcome: normalizedOutcome,
});
}
function normalizeObservabilityVerificationSummary(inputPayload, evidencePayload) {
const observabilitySummary = resolveObservabilitySummary(
inputPayload,
evidencePayload,
);
const verificationSummary = isObject(observabilitySummary?.verificationSummary)
? observabilitySummary.verificationSummary
: null;
if (!verificationSummary) {
return [];
}
const outcomes = [];
const seen = new Set();
const artifactValidator = isObject(verificationSummary.artifactValidator)
? verificationSummary.artifactValidator
: null;
if (artifactValidator?.applicable === true) {
const recordCount = Number(artifactValidator.recordCount) || 0;
const issueCount = Number(artifactValidator.issueCount) || 0;
const repairedCount = Number(artifactValidator.repairedCount) || 0;
const fallbackUsedCount = Number(artifactValidator.fallbackUsedCount) || 0;
if (recordCount > 0 && issueCount === 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"artifactValidator",
"clean",
);
}
if (issueCount > 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"artifactValidator",
"issues_present",
);
}
if (repairedCount > 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"artifactValidator",
"repaired",
);
}
if (fallbackUsedCount > 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"artifactValidator",
"fallback_used",
);
}
}
const browserVerification = isObject(verificationSummary.browserVerification)
? verificationSummary.browserVerification
: null;
if (browserVerification) {
if ((Number(browserVerification.successCount) || 0) > 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"browserVerification",
"success",
);
}
if ((Number(browserVerification.failureCount) || 0) > 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"browserVerification",
"failure",
);
}
if ((Number(browserVerification.unknownCount) || 0) > 0) {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"browserVerification",
"unknown",
);
}
}
const guiSmoke = isObject(verificationSummary.guiSmoke)
? verificationSummary.guiSmoke
: null;
if (guiSmoke && typeof guiSmoke.passed === "boolean") {
pushUniqueObservabilityVerificationOutcome(
outcomes,
seen,
"guiSmoke",
guiSmoke.passed ? "passed" : "failed",
);
}
return outcomes;
}
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 observabilityVerificationCoverage =
normalizeObservabilityVerificationSummary(inputPayload, evidencePayload);
const observabilityVerificationOutcomes =
observabilityVerificationCoverage.map(
(entry) => `${entry.signal}:${entry.outcome}`,
);
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,
observabilityVerificationCoverage,
observabilityVerificationOutcomes,
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,
observabilityVerificationCoverage: [],
observabilityVerificationOutcomes: [],
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,
observabilityVerificationCoverage: [],
observabilityVerificationOutcomes: [],
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,
);
const currentObservabilityGapCases = observabilityGapCases.filter((entry) =>
isCurrentObservabilityGapCase(entry),
);
const degradedObservabilityGapCases = observabilityGapCases.filter((entry) =>
isDegradedObservabilityGapCase(entry),
);
const currentObservabilityDiagnosticCases = allCases.filter((entry) =>
isCurrentObservabilityDiagnosticCase(entry),
);
const currentRecoveredObservabilityVerificationOutcomes =
aggregateCaseBreakdown(
currentObservabilityDiagnosticCases,
(entry) =>
collectRecoveredVerificationOutcomes(
entry.observabilityVerificationOutcomes,
),
);
const currentRecoveredVerificationCaseCount =
currentRecoveredObservabilityVerificationOutcomes.reduce(
(total, entry) => total + entry.caseCount,
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,
currentObservabilityGapCaseCount: currentObservabilityGapCases.length,
degradedObservabilityGapCaseCount: degradedObservabilityGapCases.length,
currentRecoveredVerificationCaseCount,
},
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,
),
observabilityVerificationOutcomes: aggregateCaseBreakdown(
allCases,
(entry) => entry.observabilityVerificationOutcomes,
),
currentObservabilityVerificationOutcomes: aggregateCaseBreakdown(
currentObservabilityDiagnosticCases,
(entry) => entry.observabilityVerificationOutcomes,
),
currentRecoveredObservabilityVerificationOutcomes,
degradedObservabilityVerificationOutcomes: aggregateCaseBreakdown(
allCases.filter((entry) => isDegradedObservabilityDiagnosticCase(entry)),
(entry) => entry.observabilityVerificationOutcomes,
),
},
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}`,
`[harness-eval] current observability-gap cases: ${summary.totals.currentObservabilityGapCaseCount}`,
`[harness-eval] degraded observability-gap cases: ${summary.totals.degradedObservabilityGapCaseCount}`,
`[harness-eval] current recovered verification cases: ${summary.totals.currentRecoveredVerificationCaseCount}`,
];
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}`,
);
}
}
const topVerificationOutcomes =
summary.breakdowns.observabilityVerificationOutcomes.slice(0, 5);
if (topVerificationOutcomes.length > 0) {
lines.push("[harness-eval] observability verification outcomes:");
for (const entry of topVerificationOutcomes) {
lines.push(
` - ${entry.name}: case=${entry.caseCount}, ready=${entry.readyCount}, invalid=${entry.invalidCount}`,
);
}
}
const topCurrentRecoveredVerificationOutcomes =
summary.breakdowns.currentRecoveredObservabilityVerificationOutcomes.slice(
0,
5,
);
if (topCurrentRecoveredVerificationOutcomes.length > 0) {
lines.push("[harness-eval] current recovered verification outcomes:");
for (const entry of topCurrentRecoveredVerificationOutcomes) {
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(", ")}`,
);
}
if (entry.observabilityVerificationOutcomes.length > 0) {
lines.push(
` verification: ${entry.observabilityVerificationOutcomes.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}`,
`- current observability gap case${summary.totals.currentObservabilityGapCaseCount}`,
`- degraded observability gap case${summary.totals.degradedObservabilityGapCaseCount}`,
`- current recovered verification case${summary.totals.currentRecoveredVerificationCaseCount}`,
"",
];
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("");
}
if (summary.breakdowns.observabilityVerificationOutcomes.length > 0) {
lines.push("## Observability Verification Outcome 分布");
lines.push("");
lines.push("| Outcome | case | ready | invalid |");
lines.push("| --- | --- | --- | --- |");
for (const entry of summary.breakdowns.observabilityVerificationOutcomes) {
lines.push(
`| ${entry.name} | ${entry.caseCount} | ${entry.readyCount} | ${entry.invalidCount} |`,
);
}
lines.push("");
}
if (summary.breakdowns.currentRecoveredObservabilityVerificationOutcomes.length > 0) {
lines.push("## Current Recovered Verification Outcome 分布");
lines.push("");
lines.push("| Outcome | case | ready | invalid |");
lines.push("| --- | --- | --- | --- |");
for (const entry of summary.breakdowns.currentRecoveredObservabilityVerificationOutcomes) {
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("<br>");
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(", ")}`,
);
}
if (entry.observabilityVerificationOutcomes.length > 0) {
classificationText.push(
`verification: ${entry.observabilityVerificationOutcomes.join(", ")}`,
);
}
const reviewText = entry.reviewDecisionStatus
? [
entry.reviewDecisionStatus,
entry.reviewRiskLevel ? `risk: ${entry.reviewRiskLevel}` : "",
entry.reviewHumanReviewer
? `by: ${entry.reviewHumanReviewer}`
: "",
]
.filter(Boolean)
.join("<br>")
: "无";
lines.push(
`| ${entry.caseId} | ${entry.status} | ${entry.source} | ${classificationText.join("<br>") || "无"} | ${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);
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);
}
const exitCode = determineExitCode(summary, options);
if (exitCode !== 0) {
process.exit(exitCode);
}
}
main();