feat: finalize latest v0.97.0 updates

This commit is contained in:
coso
2026-03-27 19:22:46 +08:00
parent b06c4a0408
commit 91dc5dd1ca
64 changed files with 9744 additions and 2399 deletions
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import legacySurfaceCatalog from "../src/lib/governance/legacySurfaceCatalog.json" with { type: "json" };
import {
DOC_FRESHNESS_SPECS,
buildDocFreshnessReport,
renderDocFreshnessText,
} from "./lib/doc-freshness-core.mjs";
export {
DOC_FRESHNESS_SPECS,
buildDocFreshnessReport,
renderDocFreshnessText,
} from "./lib/doc-freshness-core.mjs";
function parseArgs(argv) {
const options = {
format: "text",
help: false,
output: "",
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--format" && argv[index + 1]) {
options.format = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--output" && argv[index + 1]) {
options.output = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--json") {
options.format = "json";
continue;
}
if (arg === "--help" || arg === "-h") {
options.help = true;
}
}
return options;
}
function printHelp() {
console.log(`
Lime Doc Freshness Check
用法:
node scripts/check-doc-freshness.mjs
node scripts/check-doc-freshness.mjs --json
node scripts/check-doc-freshness.mjs --format json --output "./tmp/doc-freshness.json"
选项:
--format FMT 标准输出格式:text | json
--json 等价于 --format json
--output PATH 将结果写入指定路径
-h, --help 显示帮助
`);
}
function buildDeletedSurfaceTargets(repoRoot) {
return (legacySurfaceCatalog.imports ?? [])
.flatMap((entry) => entry.targets ?? [])
.filter((target) => {
const absolutePath = path.resolve(repoRoot, target);
return !fs.existsSync(absolutePath);
});
}
function readMonitoredDocuments(repoRoot) {
return DOC_FRESHNESS_SPECS.flatMap((spec) => {
const absolutePath = path.resolve(repoRoot, spec.path);
if (!fs.existsSync(absolutePath)) {
return [];
}
return [
{
path: spec.path,
content: fs.readFileSync(absolutePath, "utf8"),
},
];
});
}
export function buildLiveDocFreshnessReport(repoRoot) {
return buildDocFreshnessReport({
repoRoot,
documents: readMonitoredDocuments(repoRoot),
deletedSurfaceTargets: buildDeletedSurfaceTargets(repoRoot),
pathExists: (absolutePath) => fs.existsSync(absolutePath),
});
}
function renderOutput(report, format) {
if (format === "json") {
return `${JSON.stringify(report, null, 2)}\n`;
}
return renderDocFreshnessText(report);
}
function runDocFreshnessCli() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
const repoRoot = process.cwd();
const report = buildLiveDocFreshnessReport(repoRoot);
const output = renderOutput(report, options.format);
if (options.output) {
const targetPath = path.resolve(repoRoot, options.output);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, output, "utf8");
console.log(`[lime] doc freshness output: ${targetPath}`);
} else {
process.stdout.write(output);
}
if (report.summary.issueCount > 0) {
process.exit(1);
}
}
const isMainModule =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMainModule) {
runDocFreshnessCli();
}
+58 -26
View File
@@ -8,7 +8,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { cruise } from "dependency-cruiser";
import extractTSConfig from "dependency-cruiser/config-utl/extract-ts-config";
import YAML from "yaml";
import { buildLegacySurfaceReport } from "./report-legacy-surfaces.mjs";
import { buildLegacySurfaceReport } from "./lib/legacy-surface-report-core.mjs";
import {
buildRustModuleIndex,
buildRustModulePathFromFile,
@@ -138,7 +138,10 @@ function parseArgs(argv) {
}
if (arg === "--days" && argv[index + 1]) {
result.days = normalizePositiveNumber(argv[index + 1], DEFAULT_SINCE_DAYS);
result.days = normalizePositiveNumber(
argv[index + 1],
DEFAULT_SINCE_DAYS,
);
index += 1;
}
}
@@ -217,8 +220,10 @@ async function buildGovernanceGraphReport({
const signalsSummary = summarizeSignals(nodes);
const summary = buildSummary(nodes, edges, signalsSummary);
const links = {
selfHtmlHref: pathToFileURL(path.join(outputDir, "governance-graph.html")).href,
selfJsonHref: pathToFileURL(path.join(outputDir, "governance-graph.json")).href,
selfHtmlHref: pathToFileURL(path.join(outputDir, "governance-graph.html"))
.href,
selfJsonHref: pathToFileURL(path.join(outputDir, "governance-graph.json"))
.href,
heatmapHtmlHref: fs.existsSync(path.join(outputDir, "index.html"))
? pathToFileURL(path.join(outputDir, "index.html")).href
: "",
@@ -280,7 +285,9 @@ function collectGovernanceFiles(repoRoot) {
const ext = path.extname(relativePath).toLowerCase();
const language = ext === ".rs" ? "rs" : ext.replace(/^\./, "");
const layer = relativePath.startsWith("src-tauri/src/") ? "rust" : "frontend";
const layer = relativePath.startsWith("src-tauri/src/")
? "rust"
: "frontend";
records.push({
path: relativePath,
@@ -399,7 +406,10 @@ function collectGitChurn({ gitCommand, repoRoot, sinceDays, trackedFiles }) {
continue;
}
fileChurn.set(normalizedGitPath, (fileChurn.get(normalizedGitPath) || 0) + churn);
fileChurn.set(
normalizedGitPath,
(fileChurn.get(normalizedGitPath) || 0) + churn,
);
}
return fileChurn;
@@ -469,7 +479,9 @@ async function collectFrontendDependencies(repoRoot, fileIndex) {
}
function collectRustDependencies(repoRoot, fileInventory, fileIndex) {
const rustFiles = fileInventory.filter((fileRecord) => fileRecord.layer === "rust");
const rustFiles = fileInventory.filter(
(fileRecord) => fileRecord.layer === "rust",
);
const rustFileSet = new Set(rustFiles.map((fileRecord) => fileRecord.path));
const moduleIndex = buildRustModuleIndex([...rustFileSet]);
const edges = [];
@@ -477,7 +489,8 @@ function collectRustDependencies(repoRoot, fileInventory, fileIndex) {
for (const fileRecord of rustFiles) {
const absolutePath = path.join(repoRoot, fileRecord.path);
const sourceCode = fs.readFileSync(absolutePath, "utf8");
const currentModulePath = buildRustModulePathFromFile(fileRecord.path) || "";
const currentModulePath =
buildRustModulePathFromFile(fileRecord.path) || "";
for (const moduleName of extractRustModDeclarations(sourceCode)) {
const targetPath = resolveRustSubmodulePath(
@@ -661,7 +674,10 @@ function buildLegacyOverlays(legacyReport) {
}
}
for (const result of [...legacyReport.frontendTextResults, ...legacyReport.rustTextResults]) {
for (const result of [
...legacyReport.frontendTextResults,
...legacyReport.rustTextResults,
]) {
for (const callerPath of result.references) {
pushOverlay(callsiteOverlays, callerPath, {
source: "legacy-report",
@@ -674,7 +690,9 @@ function buildLegacyOverlays(legacyReport) {
}
for (const result of legacyReport.rustTextCountResults) {
for (const callerPath of result.runtimeMatches.map((item) => item.relativePath)) {
for (const callerPath of result.runtimeMatches.map(
(item) => item.relativePath,
)) {
pushOverlay(callsiteOverlays, callerPath, {
source: "legacy-report",
overlayType: "callsite",
@@ -727,7 +745,8 @@ function buildFileNodes({
legacyOverlays.callsiteOverlays.get(fileRecord.path) ?? [];
const explicitStatus = matchingRule?.status ?? "unclassified";
const overlayStatus = pickOverlayStatus(surfaceOverlays);
const status = explicitStatus !== "unclassified" ? explicitStatus : overlayStatus;
const status =
explicitStatus !== "unclassified" ? explicitStatus : overlayStatus;
const ruleSource = matchingRule
? {
reason: matchingRule.reason || "",
@@ -743,12 +762,16 @@ function buildFileNodes({
signals.add("unused-file");
}
if ((knipSignals.unusedExportsByFile.get(fileRecord.path) ?? []).length > 0) {
if (
(knipSignals.unusedExportsByFile.get(fileRecord.path) ?? []).length > 0
) {
signals.add("unused-export");
}
if (
surfaceOverlays.some((overlay) => overlay.classification === "dead-candidate")
surfaceOverlays.some(
(overlay) => overlay.classification === "dead-candidate",
)
) {
signals.add("dead-candidate");
}
@@ -801,11 +824,7 @@ function finalizeNodeSignals(nodes, edges, governanceRules) {
return nodes.map((node) => {
const signals = new Set(node.signals);
if (
!node.reachable &&
node.kind === "page" &&
node.layer === "frontend"
) {
if (!node.reachable && node.kind === "page" && node.layer === "frontend") {
signals.add("page-unreachable");
}
@@ -861,7 +880,10 @@ function pickOverlayStatus(surfaceOverlays) {
}
function calculateNodeSize(loc) {
return Math.max(26, Math.min(86, Math.round(18 + Math.sqrt(Math.max(loc, 1)))));
return Math.max(
26,
Math.min(86, Math.round(18 + Math.sqrt(Math.max(loc, 1)))),
);
}
function createParentDirectoryId(relativePath) {
@@ -901,7 +923,9 @@ function buildDirectoryNodes(fileNodes) {
}
}
return [...dirMap.values()].sort((left, right) => left.path.localeCompare(right.path));
return [...dirMap.values()].sort((left, right) =>
left.path.localeCompare(right.path),
);
}
function collectReachableFiles(entryPaths, edges) {
@@ -966,7 +990,10 @@ function isProtectedEntryNode(node, governanceRules) {
return true;
}
return resolveMatchingGovernanceRule(node.path, governanceRules)?.status === "current";
return (
resolveMatchingGovernanceRule(node.path, governanceRules)?.status ===
"current"
);
}
function calculateCandidateScore(node) {
@@ -1003,16 +1030,21 @@ function summarizeSignals(nodes) {
return [...counts.entries()]
.map(([signal, count]) => ({ signal, count }))
.sort((left, right) => right.count - left.count || left.signal.localeCompare(right.signal));
.sort(
(left, right) =>
right.count - left.count || left.signal.localeCompare(right.signal),
);
}
function buildSummary(nodes, edges, signalsSummary) {
const fileNodes = nodes.filter((node) => node.kind !== "dir");
const statusCounts = Object.fromEntries(
["current", "compat", "deprecated", "dead", "unclassified"].map((status) => [
status,
fileNodes.filter((node) => node.status === status).length,
]),
["current", "compat", "deprecated", "dead", "unclassified"].map(
(status) => [
status,
fileNodes.filter((node) => node.status === status).length,
],
),
);
const layerCounts = Object.fromEntries(
["frontend", "rust"].map((layer) => [
+343
View File
@@ -0,0 +1,343 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const RUNNER_PATH = "scripts/harness-eval-runner.mjs";
const TREND_PATH = "scripts/harness-eval-trend-report.mjs";
const CLEANUP_PATH = "scripts/report-generated-slop.mjs";
function parseArgs(argv) {
const result = {
cleanupJson: "",
cleanupMarkdown: "",
format: "text",
help: false,
historyDir: "./artifacts/history",
outputJson: "",
retain: 30,
skipCleanup: false,
skipTrend: false,
trendJson: "",
trendMarkdown: "",
workspaceRoot: process.cwd(),
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--history-dir" && argv[index + 1]) {
result.historyDir = 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 === "--retain" && argv[index + 1]) {
result.retain = Number.parseInt(String(argv[index + 1]).trim(), 10);
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 === "--trend-json" && argv[index + 1]) {
result.trendJson = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--trend-markdown" && argv[index + 1]) {
result.trendMarkdown = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--cleanup-json" && argv[index + 1]) {
result.cleanupJson = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--cleanup-markdown" && argv[index + 1]) {
result.cleanupMarkdown = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--skip-trend") {
result.skipTrend = true;
continue;
}
if (arg === "--skip-cleanup") {
result.skipCleanup = true;
continue;
}
if (arg === "--help" || arg === "-h") {
result.help = true;
}
}
return result;
}
function printHelp() {
console.log(`
Lime Harness Eval History Record
用法:
node scripts/harness-eval-history-record.mjs
node scripts/harness-eval-history-record.mjs --workspace-root "/path/to/workspace"
node scripts/harness-eval-history-record.mjs --history-dir "./artifacts/history" --output-json "./tmp/harness-history-record.json"
选项:
--history-dir PATH summary 历史目录,默认 ./artifacts/history
--workspace-root PATH 生成当前 summary 时使用的工作区根目录
--retain N 历史窗口保留数量,默认 30
--trend-json PATH trend JSON 输出路径
--trend-markdown PATH trend Markdown 输出路径
--cleanup-json PATH cleanup JSON 输出路径
--cleanup-markdown PATH cleanup Markdown 输出路径
--skip-trend 只记录 summary,不生成 trend
--skip-cleanup 只记录 summary / trend,不生成 cleanup
--format FMT 标准输出格式:text | json
--output-json PATH 将记录结果写入指定路径
-h, --help 显示帮助
`);
}
function resolvePath(baseDir, targetPath) {
return path.resolve(baseDir, targetPath);
}
function ensureParentDirectory(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function timestampForFilename() {
return new Date().toISOString().replace(/[-:]/g, "").replace(/\./g, "");
}
function collectHistoryFiles(historyDir) {
if (!fs.existsSync(historyDir)) {
return [];
}
return fs
.readdirSync(historyDir)
.filter((entry) => entry.endsWith(".json"))
.map((entry) => path.join(historyDir, entry))
.sort((left, right) => left.localeCompare(right));
}
function buildCurrentSummary(repoRoot, workspaceRoot) {
const runnerPath = resolvePath(repoRoot, RUNNER_PATH);
const output = execFileSync(
process.execPath,
[runnerPath, "--format", "json", "--workspace-root", workspaceRoot],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
},
);
return JSON.parse(output);
}
function writeJsonFile(filePath, payload) {
ensureParentDirectory(filePath);
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
}
function trimHistoryFiles(historyDir, retain) {
const files = collectHistoryFiles(historyDir).sort((left, right) =>
right.localeCompare(left),
);
const removable = files.slice(Math.max(retain, 0));
for (const filePath of removable) {
fs.rmSync(filePath, { force: true });
}
return removable;
}
function buildDefaultArtifactPaths(historyDir) {
const artifactsRoot = path.dirname(historyDir);
return {
trendJson: path.join(artifactsRoot, "harness-eval-trend.json"),
trendMarkdown: path.join(artifactsRoot, "harness-eval-trend.md"),
cleanupJson: path.join(artifactsRoot, "harness-cleanup-report.json"),
cleanupMarkdown: path.join(artifactsRoot, "harness-cleanup-report.md"),
};
}
function renderOutput(result, format) {
if (format === "json") {
return `${JSON.stringify(result, null, 2)}\n`;
}
const lines = [
"[lime] harness eval history record",
`[lime] history dir: ${result.historyDir}`,
`[lime] recorded summary: ${result.recordedSummaryPath}`,
`[lime] history count: ${result.historyCount}`,
`[lime] trimmed files: ${result.trimmedPaths.length}`,
];
if (result.trend) {
lines.push(`[lime] trend sample count: ${result.trend.sampleCount}`);
if (result.trend.outputJsonPath) {
lines.push(`[lime] trend json: ${result.trend.outputJsonPath}`);
}
}
if (result.cleanup) {
lines.push(
`[lime] cleanup trend samples: ${result.cleanup.trendSampleCount}`,
);
if (result.cleanup.outputJsonPath) {
lines.push(`[lime] cleanup json: ${result.cleanup.outputJsonPath}`);
}
}
return `${lines.join("\n")}\n`;
}
function runHistoryRecordCli() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
const repoRoot = process.cwd();
const historyDir = resolvePath(repoRoot, options.historyDir);
fs.mkdirSync(historyDir, { recursive: true });
const summary = buildCurrentSummary(repoRoot, options.workspaceRoot);
const summaryFilePath = path.join(
historyDir,
`${timestampForFilename()}-harness-eval-summary.json`,
);
writeJsonFile(summaryFilePath, summary);
const trimmedPaths = trimHistoryFiles(historyDir, options.retain);
const historyCount = collectHistoryFiles(historyDir).length;
const defaults = buildDefaultArtifactPaths(historyDir);
const result = {
recordedAt: new Date().toISOString(),
historyDir,
recordedSummaryPath: summaryFilePath,
historyCount,
trimmedPaths,
trend: null,
cleanup: null,
};
if (!options.skipTrend) {
const trendJsonPath = resolvePath(
repoRoot,
options.trendJson || defaults.trendJson,
);
const trendMarkdownPath = resolvePath(
repoRoot,
options.trendMarkdown || defaults.trendMarkdown,
);
const trendScriptPath = resolvePath(repoRoot, TREND_PATH);
const trendOutput = execFileSync(
process.execPath,
[
trendScriptPath,
"--format",
"json",
"--history-dir",
historyDir,
"--output-json",
trendJsonPath,
"--output-markdown",
trendMarkdownPath,
],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
},
);
const trendReport = JSON.parse(trendOutput);
result.trend = {
sampleCount: trendReport.sampleCount,
outputJsonPath: trendJsonPath,
outputMarkdownPath: trendMarkdownPath,
};
}
if (!options.skipCleanup) {
const cleanupJsonPath = resolvePath(
repoRoot,
options.cleanupJson || defaults.cleanupJson,
);
const cleanupMarkdownPath = resolvePath(
repoRoot,
options.cleanupMarkdown || defaults.cleanupMarkdown,
);
const cleanupScriptPath = resolvePath(repoRoot, CLEANUP_PATH);
const cleanupOutput = execFileSync(
process.execPath,
[
cleanupScriptPath,
"--format",
"json",
"--trend-history-dir",
historyDir,
"--output-json",
cleanupJsonPath,
"--output-markdown",
cleanupMarkdownPath,
],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
},
);
const cleanupReport = JSON.parse(cleanupOutput);
result.cleanup = {
trendSampleCount: cleanupReport.summary?.trend?.sampleCount ?? 0,
outputJsonPath: cleanupJsonPath,
outputMarkdownPath: cleanupMarkdownPath,
};
}
const rendered = renderOutput(result, options.format);
if (options.outputJson) {
const outputPath = resolvePath(repoRoot, options.outputJson);
writeJsonFile(outputPath, result);
console.log(`[lime] harness eval history record JSON: ${outputPath}`);
} else {
process.stdout.write(rendered);
}
}
runHistoryRecordCli();
+277 -3
View File
@@ -5,14 +5,33 @@ 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(),
};
@@ -50,6 +69,18 @@ function parseArgs(argv) {
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;
@@ -77,6 +108,7 @@ Lime Harness Eval Runner
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
@@ -84,6 +116,8 @@ Lime Harness Eval Runner
--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 显示帮助
@@ -102,6 +136,56 @@ 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 [];
@@ -115,6 +199,20 @@ 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,
@@ -208,6 +306,88 @@ function listReplayDirectories(rootPath) {
.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 validateCaseDirectory(caseDir, caseConfig, defaults, context) {
const requiredArtifacts = normalizeStringList(
caseConfig.requiredArtifacts ?? defaults.requiredArtifacts,
@@ -237,6 +417,7 @@ function validateCaseDirectory(caseDir, caseConfig, defaults, context) {
let inputPayload = null;
let expectedPayload = null;
let evidencePayload = null;
let reviewDecision = null;
if (fs.existsSync(files["input.json"] ?? "")) {
try {
@@ -281,6 +462,13 @@ function validateCaseDirectory(caseDir, caseConfig, defaults, context) {
}
}
const reviewDecisionResult = loadReviewDecisionForCase(
resolvedCaseDir,
caseConfig.reviewDecision,
);
reviewDecision = reviewDecisionResult.reviewDecision;
issues.push(...reviewDecisionResult.issues);
const pendingRequestCount = Array.isArray(
inputPayload?.runtimeContext?.pendingRequests,
)
@@ -303,6 +491,7 @@ function validateCaseDirectory(caseDir, caseConfig, defaults, context) {
typeof expectedPayload?.graderSuggestion?.preferredMode === "string"
? expectedPayload.graderSuggestion.preferredMode
: "";
const reviewDecisionRecorded = reviewDecision != null;
return {
caseId: context.caseId,
@@ -326,6 +515,11 @@ function validateCaseDirectory(caseDir, caseConfig, defaults, context) {
inputPayload?.task?.goalSummary ?? expectedPayload?.goalSummary ?? "",
pendingRequestCount,
requiresHumanReview,
reviewDecisionRecorded,
reviewDecisionStatus: reviewDecision?.decisionStatus ?? "",
reviewRiskLevel: reviewDecision?.riskLevel ?? "",
reviewHumanReviewer: reviewDecision?.humanReviewer ?? "",
reviewReviewedAt: reviewDecision?.reviewedAt ?? "",
preferredMode,
status: issues.length === 0 ? "ready" : "invalid",
issues,
@@ -385,6 +579,11 @@ function expandSuiteCases(suiteConfig, defaults, repoRoot, workspaceRoot) {
goalSummary: "",
pendingRequestCount: 0,
requiresHumanReview: false,
reviewDecisionRecorded: false,
reviewDecisionStatus: "",
reviewRiskLevel: "",
reviewHumanReviewer: "",
reviewReviewedAt: "",
preferredMode: "",
status: "invalid",
issues: [
@@ -429,6 +628,11 @@ function expandSuiteCases(suiteConfig, defaults, repoRoot, workspaceRoot) {
goalSummary: "",
pendingRequestCount: 0,
requiresHumanReview: false,
reviewDecisionRecorded: false,
reviewDecisionStatus: "",
reviewRiskLevel: "",
reviewHumanReviewer: "",
reviewReviewedAt: "",
preferredMode: "",
status: "invalid",
issues: [`不支持的 case source: ${source || "(empty)"}`],
@@ -469,6 +673,9 @@ function buildSummary(manifest, suites, options) {
const pendingCases = allCases.filter(
(entry) => entry.pendingRequestCount > 0,
);
const recordedReviewDecisionCases = allCases.filter(
(entry) => entry.reviewDecisionRecorded,
);
return {
manifestVersion: String(manifest.manifestVersion ?? "unknown"),
@@ -484,6 +691,7 @@ function buildSummary(manifest, suites, options) {
invalidCount: invalidCases.length,
needsHumanReviewCount: reviewCases.length,
pendingRequestCaseCount: pendingCases.length,
reviewDecisionRecordedCount: recordedReviewDecisionCases.length,
},
breakdowns: {
suiteTags: aggregateCaseBreakdown(allCases, (entry) => entry.tags),
@@ -491,6 +699,12 @@ function buildSummary(manifest, suites, options) {
allCases,
(entry) => entry.failureModes,
),
reviewDecisionStatuses: aggregateCaseBreakdown(allCases, (entry) =>
entry.reviewDecisionStatus ? [entry.reviewDecisionStatus] : [],
),
reviewRiskLevels: aggregateCaseBreakdown(allCases, (entry) =>
entry.reviewRiskLevel ? [entry.reviewRiskLevel] : [],
),
},
suites,
};
@@ -506,6 +720,7 @@ function renderText(summary) {
`[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}`,
];
const topFailureModes = summary.breakdowns.failureModes.slice(0, 5);
@@ -528,6 +743,16 @@ function renderText(summary) {
}
}
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}`,
);
}
}
for (const suite of summary.suites) {
lines.push(
`[harness-eval] suite ${suite.id}: ready ${suite.stats.readyCount} / ${suite.stats.caseCount}`,
@@ -542,6 +767,11 @@ function renderText(summary) {
if (entry.failureModes.length > 0) {
lines.push(` failure_modes: ${entry.failureModes.join(", ")}`);
}
if (entry.reviewDecisionStatus) {
lines.push(
` review: ${entry.reviewDecisionStatus} / risk=${entry.reviewRiskLevel || "unknown"}`,
);
}
for (const issue of entry.issues) {
lines.push(` * ${issue}`);
}
@@ -564,6 +794,7 @@ function renderMarkdown(summary) {
`- invalid:${summary.totals.invalidCount}`,
`- pending request case:${summary.totals.pendingRequestCaseCount}`,
`- needs review case:${summary.totals.needsHumanReviewCount}`,
`- 已记录人工审核:${summary.totals.reviewDecisionRecordedCount}`,
"",
];
@@ -595,6 +826,32 @@ function renderMarkdown(summary) {
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("");
}
for (const suite of summary.suites) {
lines.push(`## ${suite.title}`);
lines.push("");
@@ -613,8 +870,8 @@ function renderMarkdown(summary) {
`- ready / total:${suite.stats.readyCount} / ${suite.stats.caseCount}`,
);
lines.push("");
lines.push("| Case | 状态 | 来源 | 分类 | 目录 | 问题 |");
lines.push("| --- | --- | --- | --- | --- | --- |");
lines.push("| Case | 状态 | 来源 | 分类 | 审核 | 目录 | 问题 |");
lines.push("| --- | --- | --- | --- | --- | --- | --- |");
for (const entry of suite.cases) {
const issueText =
entry.issues.length === 0 ? "无" : entry.issues.join("<br>");
@@ -628,8 +885,19 @@ function renderMarkdown(summary) {
if (entry.primaryBlockingKind) {
classificationText.push(`blocking: ${entry.primaryBlockingKind}`);
}
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>") || "无"} | \`${entry.relativeCaseDir || "."}\` | ${issueText} |`,
`| ${entry.caseId} | ${entry.status} | ${entry.source} | ${classificationText.join("<br>") || "无"} | ${reviewText} | \`${entry.relativeCaseDir || "."}\` | ${issueText} |`,
);
}
lines.push("");
@@ -670,6 +938,7 @@ function main() {
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);
@@ -689,6 +958,11 @@ function main() {
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);
+68 -2
View File
@@ -13,6 +13,7 @@ function parseArgs(argv) {
help: false,
historyDir: "",
inputs: [],
manifest: "",
outputJson: "",
outputMarkdown: "",
workspaceRoot: process.cwd(),
@@ -27,6 +28,12 @@ function parseArgs(argv) {
continue;
}
if (arg === "--manifest" && argv[index + 1]) {
result.manifest = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--history-dir" && argv[index + 1]) {
result.historyDir = String(argv[index + 1]).trim();
index += 1;
@@ -71,11 +78,13 @@ Lime Harness Eval Trend Report
用法:
node scripts/harness-eval-trend-report.mjs
node scripts/harness-eval-trend-report.mjs --manifest "./tmp/harness-evals.manifest.json"
node scripts/harness-eval-trend-report.mjs --input "./tmp/harness-eval-summary.json"
node scripts/harness-eval-trend-report.mjs --history-dir "./artifacts/history"
node scripts/harness-eval-trend-report.mjs --output-json "./tmp/harness-eval-trend.json" --output-markdown "./tmp/harness-eval-trend.md"
选项:
--manifest PATH 透传给 harness eval runner,覆盖默认 manifest
--input PATH 显式加入一个或多个 harness eval summary JSON
--history-dir PATH 扫描目录下的历史 summary JSON
--workspace-root PATH 未提供输入时,用该工作区生成当前 summary
@@ -138,12 +147,16 @@ function isHarnessEvalSummary(candidate) {
);
}
function buildCurrentSummary(repoRoot, workspaceRoot) {
function buildCurrentSummary(repoRoot, workspaceRoot, manifestPath) {
const nodeCommand = process.execPath;
const runnerPath = resolvePath(repoRoot, RUNNER_PATH);
const args = [runnerPath, "--format", "json", "--workspace-root", workspaceRoot];
if (manifestPath) {
args.push("--manifest", manifestPath);
}
const output = execFileSync(
nodeCommand,
[runnerPath, "--format", "json", "--workspace-root", workspaceRoot],
args,
{
cwd: repoRoot,
encoding: "utf8",
@@ -403,6 +416,9 @@ function buildTrendReport(samples, repoRoot) {
needsHumanReviewCount:
normalizeNumber(latest?.totals?.needsHumanReviewCount) -
normalizeNumber(baseline?.totals?.needsHumanReviewCount),
reviewDecisionRecordedCount:
normalizeNumber(latest?.totals?.reviewDecisionRecordedCount) -
normalizeNumber(baseline?.totals?.reviewDecisionRecordedCount),
readyRate: readyRateDelta,
},
signals: buildStatusSignals(baseline, latest, sortedSamples.length),
@@ -415,6 +431,16 @@ function buildTrendReport(samples, repoRoot) {
classificationDeltas: {
suiteTags: buildBreakdownDeltas(baseline, latest, "suiteTags"),
failureModes: buildBreakdownDeltas(baseline, latest, "failureModes"),
reviewDecisionStatuses: buildBreakdownDeltas(
baseline,
latest,
"reviewDecisionStatuses",
),
reviewRiskLevels: buildBreakdownDeltas(
baseline,
latest,
"reviewRiskLevels",
),
},
};
}
@@ -428,6 +454,7 @@ function renderText(report) {
`[harness-eval-trend] delta readyCount: ${report.delta.readyCount}`,
`[harness-eval-trend] delta invalidCount: ${report.delta.invalidCount}`,
`[harness-eval-trend] delta pendingRequestCaseCount: ${report.delta.pendingRequestCaseCount}`,
`[harness-eval-trend] delta reviewDecisionRecordedCount: ${report.delta.reviewDecisionRecordedCount}`,
`[harness-eval-trend] delta readyRate: ${(report.delta.readyRate * 100).toFixed(1)}%`,
];
@@ -448,6 +475,17 @@ function renderText(report) {
}
}
const topReviewDecisionDeltas =
report.classificationDeltas.reviewDecisionStatuses.slice(0, 5);
if (topReviewDecisionDeltas.length > 0) {
lines.push("[harness-eval-trend] review decision deltas:");
for (const entry of topReviewDecisionDeltas) {
lines.push(
` - ${entry.name}: delta_case=${entry.delta.caseCount}, delta_invalid=${entry.delta.invalidCount}`,
);
}
}
return `${lines.join("\n")}\n`;
}
@@ -468,6 +506,7 @@ function renderMarkdown(report) {
`- invalid 数变化:${report.delta.invalidCount}`,
`- pending request case 变化:${report.delta.pendingRequestCaseCount}`,
`- needs review case 变化:${report.delta.needsHumanReviewCount}`,
`- 已记录人工审核变化:${report.delta.reviewDecisionRecordedCount}`,
`- ready rate 变化:${(report.delta.readyRate * 100).toFixed(1)}%`,
"",
"## 信号",
@@ -508,6 +547,32 @@ function renderMarkdown(report) {
}
}
if (report.classificationDeltas.reviewDecisionStatuses.length > 0) {
lines.push("");
lines.push("## 人工审核状态变化");
lines.push("");
lines.push("| 审核状态 | baseline case | latest case | delta case | delta invalid |");
lines.push("| --- | --- | --- | --- | --- |");
for (const entry of report.classificationDeltas.reviewDecisionStatuses) {
lines.push(
`| ${entry.name} | ${entry.baseline.caseCount} | ${entry.latest.caseCount} | ${entry.delta.caseCount} | ${entry.delta.invalidCount} |`,
);
}
}
if (report.classificationDeltas.reviewRiskLevels.length > 0) {
lines.push("");
lines.push("## 风险等级变化");
lines.push("");
lines.push("| 风险等级 | baseline case | latest case | delta case | delta invalid |");
lines.push("| --- | --- | --- | --- | --- |");
for (const entry of report.classificationDeltas.reviewRiskLevels) {
lines.push(
`| ${entry.name} | ${entry.baseline.caseCount} | ${entry.latest.caseCount} | ${entry.delta.caseCount} | ${entry.delta.invalidCount} |`,
);
}
}
lines.push("");
lines.push("## 时间线样本");
lines.push("");
@@ -587,6 +652,7 @@ function loadSamples(options, repoRoot) {
const currentSummary = buildCurrentSummary(
repoRoot,
path.resolve(options.workspaceRoot),
options.manifest,
);
sampleEntries.push({
sourcePath: "(generated-current-summary)",
+172 -1
View File
@@ -8,12 +8,27 @@ const DEFAULT_MANIFEST_PATH = "docs/test/harness-evals.manifest.json";
const DEFAULT_FIXTURES_ROOT = "docs/test/harness-fixtures/replay";
const DEFAULT_SUITE_ID = "repo-promoted-replays";
const DEFAULT_SANITIZED_WORKSPACE_ROOT = "/workspace/lime";
const REVIEW_DECISION_JSON_FILE_NAME = "review-decision.json";
const REVIEW_DECISION_MARKDOWN_FILE_NAME = "review-decision.md";
const REQUIRED_ARTIFACTS = [
"input.json",
"expected.json",
"grader.md",
"evidence-links.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 = {
@@ -150,6 +165,10 @@ function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function readTextFile(filePath) {
return fs.readFileSync(filePath, "utf8");
}
function writeJsonFile(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
@@ -180,6 +199,20 @@ function mergeUniqueStrings(...groups) {
return [...new Set(groups.flatMap((group) => normalizeStringList(group)))];
}
function normalizeEnumString(value, allowedValues, fallback = "") {
const normalized = typeof value === "string" ? value.trim() : "";
if (allowedValues.has(normalized)) {
return normalized;
}
return fallback;
}
function normalizeOptionalString(value) {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: "";
}
function slugify(value) {
return String(value)
.trim()
@@ -322,6 +355,7 @@ function getRelativeIfInside(rootPath, absolutePath) {
function buildPromotionMetadata({
promotedAt,
replayDir,
reviewDecision,
sessionId,
workspaceRoot,
sanitizedWorkspaceRoot,
@@ -338,6 +372,10 @@ function buildPromotionMetadata({
metadata.sourceReplayDir = replayRelativeDir;
}
if (reviewDecision) {
metadata.reviewDecision = reviewDecision;
}
return metadata;
}
@@ -382,9 +420,10 @@ function buildManifestCaseEntry({
caseId,
caseTitle,
inputPayload,
reviewDecision,
targetCaseDirValue,
}) {
return {
const entry = {
id: caseId,
title: caseTitle,
source: "repo_fixture",
@@ -394,6 +433,12 @@ function buildManifestCaseEntry({
inputPayload?.classification?.suiteTags,
),
};
if (reviewDecision) {
entry.reviewDecision = reviewDecision;
}
return entry;
}
function updateManifestCase({
@@ -448,6 +493,8 @@ function writePromotedArtifacts({
expectedPayload,
graderMarkdown,
inputPayload,
reviewDecisionJson,
reviewDecisionMarkdown,
targetDir,
}) {
ensureDirectory(targetDir);
@@ -455,6 +502,19 @@ function writePromotedArtifacts({
writeJsonFile(path.join(targetDir, "expected.json"), expectedPayload);
writeJsonFile(path.join(targetDir, "evidence-links.json"), evidencePayload);
fs.writeFileSync(path.join(targetDir, "grader.md"), graderMarkdown, "utf8");
if (reviewDecisionJson) {
writeJsonFile(
path.join(targetDir, REVIEW_DECISION_JSON_FILE_NAME),
reviewDecisionJson,
);
}
if (reviewDecisionMarkdown) {
fs.writeFileSync(
path.join(targetDir, REVIEW_DECISION_MARKDOWN_FILE_NAME),
reviewDecisionMarkdown,
"utf8",
);
}
}
function toManifestCaseDirValue(repoRoot, targetDir) {
@@ -485,9 +545,100 @@ function renderText(result) {
lines.push(`[harness-replay-promote] tags: ${result.tags.join(", ")}`);
}
if (result.reviewDecisionStatus) {
lines.push(
`[harness-replay-promote] review decision: ${result.reviewDecisionStatus} / risk=${result.reviewRiskLevel || "unknown"}`,
);
}
return `${lines.join("\n")}\n`;
}
function resolveSiblingReviewArtifactPath(replayDir, fileName) {
return path.resolve(replayDir, "..", "review", fileName);
}
function normalizeReviewDecisionMetadata(reviewDecisionPayload) {
const decision =
reviewDecisionPayload &&
typeof reviewDecisionPayload === "object" &&
reviewDecisionPayload.decision &&
typeof reviewDecisionPayload.decision === "object"
? reviewDecisionPayload.decision
: {};
const decisionStatus = normalizeEnumString(
decision.decisionStatus ?? decision.decision_status,
REVIEW_DECISION_STATUS_SET,
"pending_review",
);
const riskLevel = normalizeEnumString(
decision.riskLevel ?? decision.risk_level,
REVIEW_DECISION_RISK_LEVEL_SET,
"unknown",
);
const humanReviewer = normalizeOptionalString(
decision.humanReviewer ?? decision.human_reviewer,
);
const reviewedAt = normalizeOptionalString(
decision.reviewedAt ?? decision.reviewed_at,
);
return {
decisionStatus,
riskLevel,
humanReviewer,
reviewedAt,
};
}
function loadOptionalReviewDecisionArtifacts({
replayDir,
sanitizedWorkspaceRoot,
workspaceRoot,
}) {
const reviewDecisionJsonPath = resolveSiblingReviewArtifactPath(
replayDir,
REVIEW_DECISION_JSON_FILE_NAME,
);
const reviewDecisionMarkdownPath = resolveSiblingReviewArtifactPath(
replayDir,
REVIEW_DECISION_MARKDOWN_FILE_NAME,
);
const hasJson = fs.existsSync(reviewDecisionJsonPath);
const hasMarkdown = fs.existsSync(reviewDecisionMarkdownPath);
if (!hasJson && !hasMarkdown) {
return {
metadata: null,
reviewDecisionJson: null,
reviewDecisionMarkdown: "",
};
}
const reviewDecisionJson = hasJson
? sanitizePayload(
readJsonFile(reviewDecisionJsonPath),
workspaceRoot,
sanitizedWorkspaceRoot,
)
: null;
const reviewDecisionMarkdown = hasMarkdown
? replaceWorkspaceRootInString(
readTextFile(reviewDecisionMarkdownPath),
workspaceRoot,
sanitizedWorkspaceRoot,
)
: "";
return {
metadata: reviewDecisionJson
? normalizeReviewDecisionMetadata(reviewDecisionJson)
: null,
reviewDecisionJson,
reviewDecisionMarkdown,
};
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
@@ -531,6 +682,7 @@ function main() {
const promotionMetadata = buildPromotionMetadata({
promotedAt,
replayDir,
reviewDecision: undefined,
sanitizedWorkspaceRoot: options.sanitizedWorkspaceRoot,
sessionId,
workspaceRoot,
@@ -570,6 +722,18 @@ function main() {
),
promotionMetadata,
);
const {
metadata: reviewDecisionMetadata,
reviewDecisionJson,
reviewDecisionMarkdown,
} = loadOptionalReviewDecisionArtifacts({
replayDir,
sanitizedWorkspaceRoot: options.sanitizedWorkspaceRoot,
workspaceRoot,
});
if (reviewDecisionMetadata) {
promotionMetadata.reviewDecision = reviewDecisionMetadata;
}
const fixturesRoot = resolvePath(repoRoot, options.fixturesRoot);
const targetDir = path.join(fixturesRoot, slug);
@@ -584,6 +748,7 @@ function main() {
caseId,
caseTitle: title,
inputPayload,
reviewDecision: reviewDecisionMetadata,
targetCaseDirValue: manifestCaseDir,
});
@@ -597,6 +762,8 @@ function main() {
expectedPayload,
graderMarkdown,
inputPayload,
reviewDecisionJson,
reviewDecisionMarkdown,
targetDir,
});
const manifestUpdate = updateManifestCase({
@@ -623,6 +790,10 @@ function main() {
targetCaseDir: toPortablePath(targetDir),
title,
};
if (reviewDecisionMetadata) {
result.reviewDecisionStatus = reviewDecisionMetadata.decisionStatus;
result.reviewRiskLevel = reviewDecisionMetadata.riskLevel;
}
if (options.format === "json") {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
+380
View File
@@ -0,0 +1,380 @@
import path from "node:path";
export const DOC_FRESHNESS_SPECS = [
{
path: "docs/tech/harness/README.md",
requiredMentions: [
"iteration-roadmap.md",
"review-decision-workflow.md",
"tooling-roadmap.md",
"entropy-governance-workflow.md",
],
},
{
path: "docs/tech/harness/iteration-roadmap.md",
requiredMentions: [
"review-decision-workflow.md",
"tooling-roadmap.md",
"entropy-governance-workflow.md",
"scripts/report-generated-slop.mjs",
"scripts/check-doc-freshness.mjs",
],
},
{
path: "docs/tech/harness/tooling-roadmap.md",
requiredMentions: [
"harness-evals.md",
"scripts/report-generated-slop.mjs",
"scripts/check-doc-freshness.mjs",
],
},
{
path: "docs/tech/harness/entropy-governance-workflow.md",
requiredMentions: [
"iteration-roadmap.md",
"tooling-roadmap.md",
"harness-evals.md",
"scripts/report-generated-slop.mjs",
"scripts/check-doc-freshness.mjs",
],
},
{
path: "docs/tech/harness/review-decision-workflow.md",
requiredMentions: [
"external-analysis-handoff.md",
"iteration-roadmap.md",
],
},
{
path: "docs/tech/harness/external-analysis-handoff.md",
requiredMentions: [
"iteration-roadmap.md",
"review-decision-workflow.md",
"tooling-roadmap.md",
],
},
{
path: "docs/tech/harness/implementation-blueprint.md",
requiredMentions: [
"harness-evals.md",
"scripts/report-generated-slop.mjs",
],
},
{
path: "docs/test/harness-evals.md",
requiredMentions: [
"tooling-roadmap.md",
"entropy-governance-workflow.md",
"scripts/report-generated-slop.mjs",
"scripts/check-doc-freshness.mjs",
],
},
{
path: "docs/aiprompts/governance.md",
requiredMentions: [],
},
{
path: "docs/aiprompts/quality-workflow.md",
requiredMentions: [],
},
];
const LOCAL_LINK_PATTERN = /\[[^\]]+\]\(([^)]+)\)/g;
const REPO_PATH_TOKEN_PATTERN =
/(^|[\s("'`])((?:docs|scripts|src|src-tauri|\.github)\/[A-Za-z0-9._/-]+(?:\.[A-Za-z0-9]+)?)/gm;
const REPO_ROOT_RELATIVE_PREFIXES = [
"docs/",
"scripts/",
"src/",
"src-tauri/",
".github/",
];
function normalizePath(filePath) {
return filePath.split(path.sep).join("/");
}
function stripAnchor(target) {
const anchorIndex = target.indexOf("#");
return anchorIndex >= 0 ? target.slice(0, anchorIndex) : target;
}
function cleanLinkTarget(target) {
const trimmed = String(target ?? "").trim();
if (
trimmed.length === 0 ||
trimmed.startsWith("http://") ||
trimmed.startsWith("https://") ||
trimmed.startsWith("mailto:") ||
trimmed.startsWith("#") ||
trimmed.startsWith("vscode:") ||
trimmed.startsWith("file://")
) {
return "";
}
const withoutAngleBrackets =
trimmed.startsWith("<") && trimmed.endsWith(">")
? trimmed.slice(1, -1).trim()
: trimmed;
const withoutTitle = withoutAngleBrackets.split(/\s+"/, 1)[0];
const withoutAnchor = stripAnchor(withoutTitle);
return withoutAnchor.includes("<") ? "" : withoutAnchor;
}
function resolveRepoPath({ repoRoot, documentPath, rawPath }) {
const cleanedPath = cleanLinkTarget(rawPath);
if (!cleanedPath) {
return null;
}
const normalizedRepoRoot = path.resolve(repoRoot);
let absolutePath = "";
if (path.isAbsolute(cleanedPath)) {
const normalizedAbsolute = path.resolve(cleanedPath);
if (!normalizedAbsolute.startsWith(normalizedRepoRoot)) {
return null;
}
absolutePath = normalizedAbsolute;
} else if (
REPO_ROOT_RELATIVE_PREFIXES.some((prefix) => cleanedPath.startsWith(prefix))
) {
absolutePath = path.resolve(normalizedRepoRoot, cleanedPath);
} else {
absolutePath = path.resolve(
normalizedRepoRoot,
path.dirname(documentPath),
cleanedPath,
);
}
const repoRelativePath = normalizePath(
path.relative(normalizedRepoRoot, absolutePath),
);
if (repoRelativePath.startsWith("..")) {
return null;
}
return {
absolutePath,
repoRelativePath,
};
}
function collectLocalLinks(content) {
const results = [];
for (const match of content.matchAll(LOCAL_LINK_PATTERN)) {
const rawTarget = match[1];
if (!rawTarget) {
continue;
}
results.push(rawTarget);
}
return [...new Set(results)];
}
function collectRepoPathTokens(content) {
const results = [];
for (const match of content.matchAll(REPO_PATH_TOKEN_PATTERN)) {
const rawTarget = match[2];
if (!rawTarget) {
continue;
}
results.push(rawTarget.trim());
}
return [...new Set(results)];
}
function createIssue(kind, documentPath, detail) {
return {
kind,
documentPath,
detail,
};
}
export function buildDocFreshnessReport({
repoRoot,
documents,
deletedSurfaceTargets = [],
pathExists,
specs = DOC_FRESHNESS_SPECS,
}) {
const documentsByPath = new Map(
(Array.isArray(documents) ? documents : []).map((entry) => [
normalizePath(entry.path),
String(entry.content ?? ""),
]),
);
const issues = [];
const documentReports = [];
for (const spec of specs) {
const documentPath = normalizePath(spec.path);
const content = documentsByPath.get(documentPath);
if (content == null) {
issues.push(createIssue("missing-document", documentPath, documentPath));
documentReports.push({
path: documentPath,
exists: false,
requiredMentions: [],
localLinks: [],
codePathMentions: [],
deletedSurfaceReferences: [],
});
continue;
}
const requiredMentions = (spec.requiredMentions ?? []).map((needle) => ({
needle,
found: content.includes(needle),
}));
for (const entry of requiredMentions) {
if (!entry.found) {
issues.push(
createIssue("missing-required-reference", documentPath, entry.needle),
);
}
}
const localLinks = collectLocalLinks(content).map((rawTarget) => {
const resolved = resolveRepoPath({
repoRoot,
documentPath,
rawPath: rawTarget,
});
if (!resolved) {
return {
rawTarget,
repoRelativePath: "",
exists: true,
};
}
const exists = pathExists(resolved.absolutePath, resolved.repoRelativePath);
if (!exists) {
issues.push(
createIssue(
"broken-markdown-link",
documentPath,
`${rawTarget} -> ${resolved.repoRelativePath}`,
),
);
}
return {
rawTarget,
repoRelativePath: resolved.repoRelativePath,
exists,
};
});
const codePathMentions = collectRepoPathTokens(content).map((rawTarget) => {
const resolved = resolveRepoPath({
repoRoot,
documentPath,
rawPath: rawTarget,
});
if (!resolved) {
return {
rawTarget,
repoRelativePath: "",
exists: true,
};
}
const exists = pathExists(resolved.absolutePath, resolved.repoRelativePath);
if (!exists) {
issues.push(
createIssue(
"broken-code-path-reference",
documentPath,
`${rawTarget} -> ${resolved.repoRelativePath}`,
),
);
}
return {
rawTarget,
repoRelativePath: resolved.repoRelativePath,
exists,
};
});
const deletedSurfaceReferences = [...new Set(deletedSurfaceTargets)]
.filter(Boolean)
.filter((target) => content.includes(target));
for (const target of deletedSurfaceReferences) {
issues.push(
createIssue("deleted-surface-reference", documentPath, target),
);
}
documentReports.push({
path: documentPath,
exists: true,
requiredMentions,
localLinks,
codePathMentions,
deletedSurfaceReferences,
});
}
const summary = {
monitoredDocumentCount: specs.length,
existingDocumentCount: documentReports.filter((entry) => entry.exists).length,
issueCount: issues.length,
missingDocumentCount: issues.filter((entry) => entry.kind === "missing-document")
.length,
missingRequiredReferenceCount: issues.filter(
(entry) => entry.kind === "missing-required-reference",
).length,
brokenMarkdownLinkCount: issues.filter(
(entry) => entry.kind === "broken-markdown-link",
).length,
brokenCodePathReferenceCount: issues.filter(
(entry) => entry.kind === "broken-code-path-reference",
).length,
deletedSurfaceReferenceCount: issues.filter(
(entry) => entry.kind === "deleted-surface-reference",
).length,
};
return {
reportVersion: "v1",
generatedAt: new Date().toISOString(),
repoRoot: path.resolve(repoRoot),
summary,
documents: documentReports,
issues,
};
}
export function renderDocFreshnessText(report) {
const lines = [
"[lime] doc freshness report",
`[lime] monitored docs: ${report.summary.monitoredDocumentCount}`,
`[lime] existing docs: ${report.summary.existingDocumentCount}`,
`[lime] issues: ${report.summary.issueCount}`,
`[lime] missing docs: ${report.summary.missingDocumentCount}`,
`[lime] missing required refs: ${report.summary.missingRequiredReferenceCount}`,
`[lime] broken markdown links: ${report.summary.brokenMarkdownLinkCount}`,
`[lime] broken code path refs: ${report.summary.brokenCodePathReferenceCount}`,
`[lime] deleted surface refs: ${report.summary.deletedSurfaceReferenceCount}`,
];
if (report.issues.length === 0) {
lines.push("[lime] doc freshness: clean");
return `${lines.join("\n")}\n`;
}
lines.push("[lime] issues:");
for (const issue of report.issues) {
lines.push(` - [${issue.kind}] ${issue.documentPath} -> ${issue.detail}`);
}
return `${lines.join("\n")}\n`;
}
+86
View File
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import { buildDocFreshnessReport } from "./doc-freshness-core.mjs";
describe("doc-freshness-core", () => {
it("应在回链与路径都正常时返回 clean 报告", () => {
const report = buildDocFreshnessReport({
repoRoot: "/tmp/lime",
specs: [
{
path: "docs/tech/harness/entropy-governance-workflow.md",
requiredMentions: [
"iteration-roadmap.md",
"tooling-roadmap.md",
"harness-evals.md",
"scripts/report-generated-slop.mjs",
"scripts/check-doc-freshness.mjs",
],
},
],
documents: [
{
path: "docs/tech/harness/entropy-governance-workflow.md",
content: `
[Roadmap](iteration-roadmap.md)
[Tooling](tooling-roadmap.md)
[Evals](../../test/harness-evals.md)
\`scripts/report-generated-slop.mjs\`
\`scripts/check-doc-freshness.mjs\`
`,
},
],
deletedSurfaceTargets: ["src/lib/api/agentCompat.ts"],
pathExists: (_absolutePath, repoRelativePath) =>
[
"docs/tech/harness/iteration-roadmap.md",
"docs/tech/harness/tooling-roadmap.md",
"docs/test/harness-evals.md",
"scripts/report-generated-slop.mjs",
"scripts/check-doc-freshness.mjs",
].includes(repoRelativePath),
});
expect(report.summary.issueCount).toBe(0);
expect(report.documents[0].requiredMentions.every((entry) => entry.found)).toBe(
true,
);
});
it("应识别缺失回链、坏链接、坏路径与已删除表面引用", () => {
const report = buildDocFreshnessReport({
repoRoot: "/tmp/lime",
specs: [
{
path: "docs/tech/harness/review-decision-workflow.md",
requiredMentions: [
"external-analysis-handoff.md",
"iteration-roadmap.md",
],
},
],
documents: [
{
path: "docs/tech/harness/review-decision-workflow.md",
content: `
[Bad Link](missing-doc.md)
\`scripts/missing-tool.mjs\`
旧入口:src/lib/api/agentCompat.ts
`,
},
],
deletedSurfaceTargets: ["src/lib/api/agentCompat.ts"],
pathExists: () => false,
});
expect(report.summary.issueCount).toBe(5);
expect(report.issues.map((entry) => entry.kind)).toEqual(
expect.arrayContaining([
"missing-required-reference",
"broken-markdown-link",
"broken-code-path-reference",
"deleted-surface-reference",
]),
);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,324 @@
import { describe, expect, it } from "vitest";
import { buildGeneratedSlopReport } from "./generated-slop-report-core.mjs";
describe("generated-slop-report-core", () => {
it("应把 trend seed 与活跃 legacy surface 转成治理建议", () => {
const report = buildGeneratedSlopReport({
repoRoot: "/tmp/lime",
trendReport: {
sampleCount: 1,
delta: {
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
reviewDecisionRecordedCount: 0,
readyRate: 0,
},
signals: ["样本数不足 2,当前仅形成 trend seed,还不能判断长期退化。"],
baseline: {
totals: {
needsHumanReviewCount: 0,
reviewDecisionRecordedCount: 0,
},
},
latest: {
totals: {
needsHumanReviewCount: 1,
reviewDecisionRecordedCount: 0,
},
},
classificationDeltas: {
failureModes: [
{
name: "pending_request",
baseline: {
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 1,
needsHumanReviewCount: 0,
},
latest: {
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 1,
needsHumanReviewCount: 0,
},
delta: {
caseCount: 0,
readyCount: 0,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
},
],
suiteTags: [
{
name: "conversation-runtime",
baseline: {
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 1,
needsHumanReviewCount: 0,
},
latest: {
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 1,
needsHumanReviewCount: 0,
},
delta: {
caseCount: 0,
readyCount: 0,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
},
],
reviewDecisionStatuses: [
{
name: "pending_review",
baseline: {
caseCount: 0,
readyCount: 0,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
latest: {
caseCount: 1,
readyCount: 0,
invalidCount: 1,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
delta: {
caseCount: 1,
readyCount: 0,
invalidCount: 1,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
},
],
reviewRiskLevels: [
{
name: "high",
baseline: {
caseCount: 0,
readyCount: 0,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
latest: {
caseCount: 1,
readyCount: 0,
invalidCount: 1,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
delta: {
caseCount: 1,
readyCount: 0,
invalidCount: 1,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
},
],
},
},
governanceReport: {
summary: {
zeroReferenceCandidates: [],
classificationDriftCandidates: [],
violations: [],
},
importResults: [
{
id: "team-subagent-scheduler-hook",
classification: "compat",
description: "compat hook",
existingTargets: ["src/hooks/useSubAgentScheduler.ts"],
references: [
"src/components/agent/chat/hooks/useCompatSubagentRuntime.ts",
],
testReferences: ["src/hooks/useSubAgentScheduler.test.tsx"],
violations: [],
},
],
commandResults: [],
frontendTextResults: [
{
id: "migration-setting-key-leak",
classification: "deprecated",
description: "migration keys",
references: ["src-tauri/crates/core/src/database/migration/foo.rs"],
testReferences: [],
violations: [],
},
],
rustTextResults: [],
rustTextCountResults: [],
},
docFreshnessReport: {
summary: {
monitoredDocumentCount: 10,
existingDocumentCount: 10,
issueCount: 0,
missingDocumentCount: 0,
missingRequiredReferenceCount: 0,
brokenMarkdownLinkCount: 0,
brokenCodePathReferenceCount: 0,
deletedSurfaceReferenceCount: 0,
},
issues: [],
},
sources: {
trend: { kind: "generated-current" },
docFreshness: { kind: "live-scan" },
governance: { kind: "live-scan" },
},
});
expect(report.summary.trend.isSeed).toBe(true);
expect(report.summary.trend.reviewDecisionBacklogCount).toBe(1);
expect(report.summary.docFreshness.issueCount).toBe(0);
expect(report.focus.failureModes[0].name).toBe("pending_request");
expect(report.focus.reviewDecisionStatuses[0].name).toBe("pending_review");
expect(report.focus.reviewRiskLevels[0].name).toBe("high");
expect(report.focus.governanceSurfaces[0].id).toBe(
"migration-setting-key-leak",
);
expect(report.recommendations.map((entry) => entry.id)).toEqual(
expect.arrayContaining([
"promote-high-value-replay",
"replay-and-smoke-follow-up",
"review-decision-follow-up",
"governance-cleanup-priority",
"doc-freshness-review",
]),
);
expect(
report.recommendations.find(
(entry) => entry.id === "review-decision-follow-up",
)?.focusReviewRiskLevels,
).toContain("high");
expect(
report.recommendations.find((entry) => entry.id === "doc-freshness-review")
?.commands,
).toContain("npm run harness:doc-freshness");
});
it("应把 governance 违规提升为 P0 守卫动作", () => {
const report = buildGeneratedSlopReport({
repoRoot: "/tmp/lime",
trendReport: {
sampleCount: 2,
delta: {
invalidCount: 1,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
reviewDecisionRecordedCount: 0,
readyRate: -0.5,
},
signals: ["invalid case 增加 1,存在回归候选。"],
baseline: {
totals: {
needsHumanReviewCount: 0,
reviewDecisionRecordedCount: 0,
},
},
latest: {
totals: {
needsHumanReviewCount: 0,
reviewDecisionRecordedCount: 0,
},
},
classificationDeltas: {
failureModes: [],
suiteTags: [],
reviewDecisionStatuses: [],
reviewRiskLevels: [],
},
},
governanceReport: {
summary: {
zeroReferenceCandidates: [],
classificationDriftCandidates: [],
violations: ["chat-compat -> src/foo.ts"],
},
importResults: [],
commandResults: [
{
id: "chat-compat-command",
classification: "deprecated",
description: "legacy command",
referencesByCommand: {
chat_create_session: ["src/lib/api/legacy.ts"],
},
testReferencesByCommand: {},
violations: ["chat_create_session -> src/lib/api/legacy.ts"],
},
],
frontendTextResults: [],
rustTextResults: [],
rustTextCountResults: [],
},
docFreshnessReport: {
summary: {
monitoredDocumentCount: 10,
existingDocumentCount: 9,
issueCount: 2,
missingDocumentCount: 0,
missingRequiredReferenceCount: 1,
brokenMarkdownLinkCount: 0,
brokenCodePathReferenceCount: 1,
deletedSurfaceReferenceCount: 0,
},
issues: [
{
kind: "missing-required-reference",
documentPath: "docs/tech/harness/entropy-governance-workflow.md",
detail: "harness-evals.md",
},
{
kind: "broken-code-path-reference",
documentPath: "docs/tech/harness/tooling-roadmap.md",
detail: "scripts/missing-tool.mjs",
},
],
},
sources: {
trend: { kind: "input-json" },
docFreshness: { kind: "input-json" },
governance: { kind: "input-json" },
},
});
expect(report.recommendations[0].id).toBe("contracts-and-boundary-guards");
expect(report.recommendations[0].priority).toBe("P0");
expect(report.signals).toContain("doc freshness 发现 2 个问题。");
expect(report.focus.docFreshness.issueKinds[0]).toEqual({
kind: "broken-code-path-reference",
count: 1,
});
expect(report.recommendations[0].commands).toEqual(
expect.arrayContaining([
"npm run governance:legacy-report",
"npm run test:contracts",
]),
);
expect(
report.recommendations.find((entry) => entry.id === "doc-freshness-review")
?.priority,
).toBe("P1");
});
});
@@ -0,0 +1,87 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
const repoRoot = process.cwd();
const tempRoots: string[] = [];
function createTempRoot() {
const tempRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "lime-harness-history-record-"),
);
tempRoots.push(tempRoot);
return tempRoot;
}
function runNodeScript(scriptRelativePath: string, args: string[]) {
const output = execFileSync(
process.execPath,
[path.join(repoRoot, scriptRelativePath), ...args],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
},
);
return JSON.parse(output);
}
afterEach(() => {
while (tempRoots.length > 0) {
const tempRoot = tempRoots.pop();
if (tempRoot) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}
});
describe("harness-eval-history-record", () => {
it("应记录本地 summary 历史,并生成非 seed 的 trend / cleanup", () => {
const tempRoot = createTempRoot();
const historyDir = path.join(tempRoot, "artifacts", "history");
const workspaceRoot = path.join(tempRoot, "workspace");
const firstResult = runNodeScript("scripts/harness-eval-history-record.mjs", [
"--format",
"json",
"--history-dir",
historyDir,
"--workspace-root",
workspaceRoot,
"--output-json",
path.join(tempRoot, "first.json"),
]);
const secondResult = runNodeScript(
"scripts/harness-eval-history-record.mjs",
[
"--format",
"json",
"--history-dir",
historyDir,
"--workspace-root",
workspaceRoot,
"--output-json",
path.join(tempRoot, "second.json"),
],
);
expect(firstResult.historyCount).toBe(1);
expect(firstResult.trend.sampleCount).toBe(1);
expect(secondResult.historyCount).toBe(2);
expect(secondResult.trend.sampleCount).toBe(2);
expect(secondResult.cleanup.trendSampleCount).toBe(2);
expect(fs.existsSync(secondResult.recordedSummaryPath)).toBe(true);
expect(
fs.existsSync(path.join(tempRoot, "artifacts", "harness-eval-trend.json")),
).toBe(true);
expect(
fs.existsSync(
path.join(tempRoot, "artifacts", "harness-cleanup-report.json"),
),
).toBe(true);
});
});
@@ -0,0 +1,226 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
const repoRoot = process.cwd();
const tempRoots: string[] = [];
function createTempRoot() {
const tempRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "lime-harness-history-window-"),
);
tempRoots.push(tempRoot);
return tempRoot;
}
function ensureDir(dirPath: string) {
fs.mkdirSync(dirPath, { recursive: true });
}
function writeJson(filePath: string, payload: unknown) {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
}
function writeText(filePath: string, value: string) {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, value, "utf8");
}
function sleepMs(durationMs: number) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, durationMs);
}
function createHarnessManifest(caseDir: string) {
return {
manifestVersion: "v1",
title: "Harness History Window Test Manifest",
defaults: {
requiredArtifacts: [
"input.json",
"expected.json",
"grader.md",
"evidence-links.json",
],
requiredInputFields: [
"session.sessionId",
"session.threadId",
"task.goalSummary",
"classification.suiteTags",
"classification.failureModes",
"linkedArtifacts.handoffBundle.relativeRoot",
"linkedArtifacts.evidencePack.relativeRoot",
],
requiredExpectedFields: [
"successCriteria",
"blockingChecks",
"artifactChecks",
"graderSuggestion.preferredMode",
],
requiredEvidenceFields: [
"handoffBundle.relativeRoot",
"evidencePack.relativeRoot",
],
},
suites: [
{
id: "repo-fixtures",
title: "仓库固定 Replay 样本",
cases: [
{
id: "fixture-history-window",
title: "Harness 历史窗口样本",
source: "repo_fixture",
caseDir,
},
],
},
],
};
}
function createReplayFixture(tempRoot: string) {
const caseDir = path.join(tempRoot, "fixture-case");
const handoffRoot = path.join(tempRoot, "handoff");
const evidenceRoot = path.join(tempRoot, "evidence");
writeJson(path.join(caseDir, "input.json"), {
session: {
sessionId: "fixture-history-session",
threadId: "fixture-history-thread",
},
task: {
goalSummary: "验证 harness eval history window",
},
classification: {
suiteTags: ["conversation-runtime", "replay"],
failureModes: ["pending_request"],
},
linkedArtifacts: {
handoffBundle: {
relativeRoot: ".lime/handoff",
absoluteRoot: handoffRoot,
},
evidencePack: {
relativeRoot: ".lime/evidence",
absoluteRoot: evidenceRoot,
},
},
});
writeJson(path.join(caseDir, "expected.json"), {
sessionId: "fixture-history-session",
threadId: "fixture-history-thread",
goalSummary: "验证 harness eval history window",
successCriteria: ["history snapshot 可以被 trend 使用"],
blockingChecks: ["summary history 目录会保留最近窗口"],
artifactChecks: ["history dir 中存在 harness-eval-summary.json"],
graderSuggestion: {
preferredMode: "summary_only",
requiresHumanReview: false,
},
});
writeJson(path.join(caseDir, "evidence-links.json"), {
handoffBundle: {
relativeRoot: ".lime/handoff",
absoluteRoot: handoffRoot,
},
evidencePack: {
relativeRoot: ".lime/evidence",
absoluteRoot: evidenceRoot,
},
});
writeText(path.join(caseDir, "grader.md"), "# Grader\n");
return caseDir;
}
function runNodeScript(scriptRelativePath: string, args: string[]) {
const output = execFileSync(
process.execPath,
[path.join(repoRoot, scriptRelativePath), ...args],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
},
);
return JSON.parse(output);
}
afterEach(() => {
while (tempRoots.length > 0) {
const tempRoot = tempRoots.pop();
if (tempRoot) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}
});
describe("Harness eval history window", () => {
it("runner 应记录并裁剪历史窗口,trend 应复用该目录", () => {
const tempRoot = createTempRoot();
const caseDir = createReplayFixture(tempRoot);
const manifestPath = path.join(tempRoot, "manifest.json");
const historyDir = path.join(tempRoot, "history");
writeJson(manifestPath, createHarnessManifest(caseDir));
runNodeScript("scripts/harness-eval-runner.mjs", [
"--format",
"json",
"--manifest",
manifestPath,
"--record-history-dir",
historyDir,
"--history-retain",
"2",
"--no-strict",
]);
sleepMs(10);
runNodeScript("scripts/harness-eval-runner.mjs", [
"--format",
"json",
"--manifest",
manifestPath,
"--record-history-dir",
historyDir,
"--history-retain",
"2",
"--no-strict",
]);
sleepMs(10);
runNodeScript("scripts/harness-eval-runner.mjs", [
"--format",
"json",
"--manifest",
manifestPath,
"--record-history-dir",
historyDir,
"--history-retain",
"2",
"--no-strict",
]);
const historyFiles = fs
.readdirSync(historyDir)
.filter((entry) => entry.endsWith("-harness-eval-summary.json"))
.sort();
expect(historyFiles).toHaveLength(2);
const report = runNodeScript("scripts/harness-eval-trend-report.mjs", [
"--format",
"json",
"--manifest",
manifestPath,
"--history-dir",
historyDir,
]);
expect(report.sampleCount).toBe(2);
expect(report.signals).not.toContain(
"样本数不足 2,当前仅形成 trend seed,还不能判断长期退化。",
);
});
});
@@ -0,0 +1,447 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
const repoRoot = process.cwd();
const tempRoots: string[] = [];
function createTempRoot() {
const tempRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "lime-harness-review-evals-"),
);
tempRoots.push(tempRoot);
return tempRoot;
}
function ensureDir(dirPath: string) {
fs.mkdirSync(dirPath, { recursive: true });
}
function writeJson(filePath: string, payload: unknown) {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
}
function writeText(filePath: string, value: string) {
ensureDir(path.dirname(filePath));
fs.writeFileSync(filePath, value, "utf8");
}
function createHarnessManifest(overrides?: Record<string, unknown>) {
return {
manifestVersion: "v1",
title: "Test Harness Eval Manifest",
defaults: {
requiredArtifacts: [
"input.json",
"expected.json",
"grader.md",
"evidence-links.json",
],
requiredInputFields: [
"session.sessionId",
"session.threadId",
"task.goalSummary",
"classification.suiteTags",
"classification.failureModes",
"linkedArtifacts.handoffBundle.relativeRoot",
"linkedArtifacts.evidencePack.relativeRoot",
],
requiredExpectedFields: [
"successCriteria",
"blockingChecks",
"artifactChecks",
"graderSuggestion.preferredMode",
],
requiredEvidenceFields: [
"handoffBundle.relativeRoot",
"evidencePack.relativeRoot",
],
},
suites: [],
...overrides,
};
}
function createWorkspaceSessionArtifacts(tempRoot: string, sessionId: string) {
const workspaceRoot = path.join(tempRoot, "workspace");
const sessionRoot = path.join(
workspaceRoot,
".lime",
"harness",
"sessions",
sessionId,
);
const replayDir = path.join(sessionRoot, "replay");
const reviewDir = path.join(sessionRoot, "review");
ensureDir(replayDir);
ensureDir(reviewDir);
const handoffRoot = path.join(sessionRoot, "handoff");
const evidenceRoot = path.join(sessionRoot, "evidence");
const inputPayload = {
session: {
sessionId: sessionId,
threadId: `${sessionId}-thread`,
},
task: {
goalSummary: "验证 review decision 会进入 replay/eval 资产",
},
classification: {
suiteTags: ["conversation-runtime", "replay"],
failureModes: ["pending_request"],
primaryBlockingKind: "pending_request",
},
runtimeContext: {
pendingRequests: [
{
id: "req-1",
},
],
},
linkedArtifacts: {
handoffBundle: {
relativeRoot: `.lime/harness/sessions/${sessionId}/handoff`,
absoluteRoot: handoffRoot,
},
evidencePack: {
relativeRoot: `.lime/harness/sessions/${sessionId}/evidence`,
absoluteRoot: evidenceRoot,
},
},
};
const expectedPayload = {
sessionId: sessionId,
threadId: `${sessionId}-thread`,
goalSummary: "验证 review decision 会进入 replay/eval 资产",
successCriteria: ["人工审核状态进入 eval 摘要"],
blockingChecks: ["review decision 不再停留在工作区孤岛"],
artifactChecks: ["review-decision.json 被识别"],
graderSuggestion: {
preferredMode: "manual_review",
requiresHumanReview: true,
},
};
const evidencePayload = {
handoffBundle: {
relativeRoot: `.lime/harness/sessions/${sessionId}/handoff`,
absoluteRoot: handoffRoot,
},
evidencePack: {
relativeRoot: `.lime/harness/sessions/${sessionId}/evidence`,
absoluteRoot: evidenceRoot,
},
};
const reviewDecisionPayload = {
schemaVersion: "v1",
contractShape: "lime_review_decision_template",
decision: {
decisionStatus: "accepted",
decisionSummary: "确认该失败应沉淀为长期回归样本。",
chosenFixStrategy: "继续把人工审核结果挂到 replay/eval 主链。",
riskLevel: "high",
riskTags: ["runtime", "eval"],
humanReviewer: "Lime Maintainer",
reviewedAt: "2026-03-27T12:00:00Z",
followupActions: ["补 trend 汇总"],
regressionRequirements: ["npm run harness:eval"],
notes: `工作区目录 ${workspaceRoot} 已参与人工审核。`,
},
};
writeJson(path.join(replayDir, "input.json"), inputPayload);
writeJson(path.join(replayDir, "expected.json"), expectedPayload);
writeJson(path.join(replayDir, "evidence-links.json"), evidencePayload);
writeText(
path.join(replayDir, "grader.md"),
`# Grader\n\n- 工作区:\`${workspaceRoot}\`\n`,
);
writeJson(
path.join(reviewDir, "review-decision.json"),
reviewDecisionPayload,
);
writeText(
path.join(reviewDir, "review-decision.md"),
`# Review\n\n- 工作区:\`${workspaceRoot}\`\n`,
);
return {
workspaceRoot,
replayDir,
reviewDir,
};
}
function runNodeScript(scriptRelativePath: string, args: string[]) {
const output = execFileSync(
process.execPath,
[path.join(repoRoot, scriptRelativePath), ...args],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
},
);
return JSON.parse(output);
}
afterEach(() => {
while (tempRoots.length > 0) {
const tempRoot = tempRoots.pop();
if (tempRoot) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}
});
describe("Harness review decision / eval integration", () => {
it("harness-replay-promote 应复制并脱敏 review decision 制品", () => {
const tempRoot = createTempRoot();
const { workspaceRoot } = createWorkspaceSessionArtifacts(
tempRoot,
"session-promote-1",
);
const manifestPath = path.join(tempRoot, "manifest.json");
const fixturesRoot = path.join(tempRoot, "fixtures");
writeJson(
manifestPath,
createHarnessManifest({
suites: [
{
id: "repo-promoted-replays",
title: "仓库沉淀 Replay 样本",
cases: [],
},
],
}),
);
const result = runNodeScript("scripts/harness-replay-promote.mjs", [
"--workspace-root",
workspaceRoot,
"--session-id",
"session-promote-1",
"--slug",
"review-promoted-case",
"--manifest",
manifestPath,
"--fixtures-root",
fixturesRoot,
"--format",
"json",
]);
const promotedCaseDir = path.join(fixturesRoot, "review-promoted-case");
const promotedReviewJson = JSON.parse(
fs.readFileSync(
path.join(promotedCaseDir, "review-decision.json"),
"utf8",
),
);
const promotedReviewMarkdown = fs.readFileSync(
path.join(promotedCaseDir, "review-decision.md"),
"utf8",
);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const caseEntry = manifest.suites[0].cases[0];
expect(result.reviewDecisionStatus).toBe("accepted");
expect(promotedReviewJson.decision.decisionStatus).toBe("accepted");
expect(promotedReviewJson.decision.notes).toContain("/workspace/lime");
expect(promotedReviewMarkdown).toContain("/workspace/lime");
expect(caseEntry.reviewDecision).toMatchObject({
decisionStatus: "accepted",
riskLevel: "high",
humanReviewer: "Lime Maintainer",
});
});
it("harness-eval-runner 应在 workspace replay discovery 中读取 sibling review decision", () => {
const tempRoot = createTempRoot();
const { workspaceRoot } = createWorkspaceSessionArtifacts(
tempRoot,
"session-runner-1",
);
const manifestPath = path.join(tempRoot, "manifest.json");
writeJson(
manifestPath,
createHarnessManifest({
suites: [
{
id: "workspace-replay-discovery",
title: "工作区 Replay 自动发现",
cases: [
{
id: "workspace-session-replays",
title: "工作区会话 Replay 样本",
source: "workspace_replay_discovery",
root: ".lime/harness/sessions",
allowZeroMatches: false,
tags: ["workspace", "replay"],
},
],
},
],
}),
);
const summary = runNodeScript("scripts/harness-eval-runner.mjs", [
"--format",
"json",
"--manifest",
manifestPath,
"--workspace-root",
workspaceRoot,
"--no-strict",
]);
expect(summary.totals.reviewDecisionRecordedCount).toBe(1);
expect(summary.breakdowns.reviewDecisionStatuses).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "accepted",
caseCount: 1,
}),
]),
);
expect(summary.breakdowns.reviewRiskLevels).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "high",
caseCount: 1,
}),
]),
);
expect(summary.suites[0].cases[0]).toMatchObject({
reviewDecisionStatus: "accepted",
reviewRiskLevel: "high",
reviewHumanReviewer: "Lime Maintainer",
});
});
it("harness-eval-trend-report 应聚合人工审核状态与风险等级变化", () => {
const tempRoot = createTempRoot();
const baselinePath = path.join(tempRoot, "baseline.json");
const latestPath = path.join(tempRoot, "latest.json");
writeJson(baselinePath, {
generatedAt: "2026-03-27T10:00:00Z",
totals: {
suiteCount: 1,
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
breakdowns: {
suiteTags: [],
failureModes: [],
reviewDecisionStatuses: [
{
name: "needs_more_evidence",
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
],
reviewRiskLevels: [
{
name: "high",
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 1,
},
],
},
suites: [],
});
writeJson(latestPath, {
generatedAt: "2026-03-27T12:00:00Z",
totals: {
suiteCount: 1,
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
breakdowns: {
suiteTags: [],
failureModes: [],
reviewDecisionStatuses: [
{
name: "accepted",
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
],
reviewRiskLevels: [
{
name: "medium",
caseCount: 1,
readyCount: 1,
invalidCount: 0,
pendingRequestCaseCount: 0,
needsHumanReviewCount: 0,
},
],
},
suites: [],
});
const report = runNodeScript("scripts/harness-eval-trend-report.mjs", [
"--format",
"json",
"--input",
baselinePath,
"--input",
latestPath,
]);
expect(report.classificationDeltas.reviewDecisionStatuses).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "accepted",
delta: expect.objectContaining({
caseCount: 1,
}),
}),
expect.objectContaining({
name: "needs_more_evidence",
delta: expect.objectContaining({
caseCount: -1,
}),
}),
]),
);
expect(report.classificationDeltas.reviewRiskLevels).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "high",
delta: expect.objectContaining({
caseCount: -1,
}),
}),
expect.objectContaining({
name: "medium",
delta: expect.objectContaining({
caseCount: 1,
}),
}),
]),
);
});
});
+688
View File
@@ -0,0 +1,688 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import legacySurfaceCatalog from "../../src/lib/governance/legacySurfaceCatalog.json" with { type: "json" };
import {
buildLegacySurfaceSummary,
getCommandStatus,
getImportStatus,
getTextCountStatus,
getTextStatus,
serializeMapEntries,
} from "./legacy-surface-report-summary.mjs";
const repoRoot = path.resolve(process.cwd());
const sourceRoots = ["src"];
const sourceExtensions = new Set([
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
]);
const rustSourceRoots = ["src-tauri/src", "src-tauri/crates"];
const rustSourceExtensions = new Set([".rs"]);
const ignoredDirs = new Set([
"node_modules",
"dist",
"build",
"coverage",
"target",
".git",
".turbo",
".next",
]);
const {
imports: importSurfaceMonitors,
commands: commandSurfaceMonitors,
frontendText: frontendTextSurfaceMonitors,
rustText: rustTextSurfaceMonitors,
rustTextCounts: rustTextCountMonitors,
} = legacySurfaceCatalog;
function normalizePath(filePath) {
return filePath.split(path.sep).join("/");
}
function resolveExistingSourcePath(absolutePath) {
if (fs.existsSync(absolutePath)) {
const stats = fs.statSync(absolutePath);
if (stats.isFile()) {
return absolutePath;
}
}
if (!path.extname(absolutePath)) {
for (const extension of sourceExtensions) {
const fileCandidate = `${absolutePath}${extension}`;
if (fs.existsSync(fileCandidate) && fs.statSync(fileCandidate).isFile()) {
return fileCandidate;
}
}
}
for (const extension of sourceExtensions) {
const indexCandidate = path.join(absolutePath, `index${extension}`);
if (fs.existsSync(indexCandidate) && fs.statSync(indexCandidate).isFile()) {
return indexCandidate;
}
}
return null;
}
function resolveImportPath(importerRelativePath, specifier) {
let absoluteCandidate = null;
if (specifier.startsWith("@/")) {
absoluteCandidate = path.join(repoRoot, "src", specifier.slice(2));
} else if (specifier.startsWith(".")) {
absoluteCandidate = path.resolve(
path.dirname(path.join(repoRoot, importerRelativePath)),
specifier,
);
}
if (!absoluteCandidate) {
return null;
}
const resolvedPath = resolveExistingSourcePath(absoluteCandidate);
if (!resolvedPath) {
return null;
}
return normalizePath(path.relative(repoRoot, resolvedPath));
}
function isTestFile(relativePath) {
return (
/(^|\/)tests(\/|$)/.test(relativePath) ||
/(^|\/)(__tests__|__mocks__)(\/|$)/.test(relativePath) ||
/\.(test|spec)\.[^/.]+$/.test(relativePath)
);
}
function walkDirectory(directoryPath, extensions) {
const files = [];
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
if (ignoredDirs.has(entry.name)) {
continue;
}
const fullPath = path.join(directoryPath, entry.name);
if (entry.isDirectory()) {
files.push(...walkDirectory(fullPath, extensions));
continue;
}
if (!extensions.has(path.extname(entry.name))) {
continue;
}
files.push(fullPath);
}
return files;
}
function extractImportSpecifiers(sourceCode) {
const specifiers = new Set();
const patterns = [
/\bimport\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'`]([^"'`]+)["'`]/g,
/\bexport\s+(?:type\s+)?[\s\S]*?\s+from\s+["'`]([^"'`]+)["'`]/g,
/\bimport\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g,
/\brequire\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g,
/\b(?:vi|jest)\.mock\s*\(\s*["'`]([^"'`]+)["'`]/g,
];
for (const pattern of patterns) {
for (const match of sourceCode.matchAll(pattern)) {
specifiers.add(match[1]);
}
}
return specifiers;
}
function extractInvokeCommands(sourceCode) {
const commands = new Set();
const patterns = [
/\bsafeInvoke(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
/\binvoke(?:<[^>]+>)?\s*\(\s*["'`]([^"'`]+)["'`]/g,
];
for (const pattern of patterns) {
for (const match of sourceCode.matchAll(pattern)) {
commands.add(match[1]);
}
}
return commands;
}
function stripRustTestModules(sourceCode) {
return sourceCode.replace(
/(?:^|\n)\s*#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]\s*(?:pub\s+)?mod\s+\w+\s*(?:\{[\s\S]*$|;)/m,
"\n",
);
}
function collectSources() {
const runtimeSources = [];
const testSources = [];
for (const root of sourceRoots) {
const absoluteRoot = path.join(repoRoot, root);
if (!fs.existsSync(absoluteRoot)) {
continue;
}
for (const filePath of walkDirectory(absoluteRoot, sourceExtensions)) {
const relativePath = normalizePath(path.relative(repoRoot, filePath));
const sourceCode = fs.readFileSync(filePath, "utf8");
const imports = extractImportSpecifiers(sourceCode);
const collectedSource = {
relativePath,
imports,
resolvedImports: new Set(
[...imports]
.map((specifier) => resolveImportPath(relativePath, specifier))
.filter(Boolean),
),
commands: extractInvokeCommands(sourceCode),
};
if (isTestFile(relativePath)) {
testSources.push(collectedSource);
continue;
}
runtimeSources.push(collectedSource);
}
}
return {
runtimeSources,
testSources,
};
}
function collectTextSources(roots, extensions) {
const runtimeSources = [];
const testSources = [];
for (const root of roots) {
const absoluteRoot = path.join(repoRoot, root);
if (!fs.existsSync(absoluteRoot)) {
continue;
}
for (const filePath of walkDirectory(absoluteRoot, extensions)) {
const relativePath = normalizePath(path.relative(repoRoot, filePath));
const sourceCode = fs.readFileSync(filePath, "utf8");
const collectedSource = {
relativePath,
sourceCode:
path.extname(relativePath) === ".rs"
? stripRustTestModules(sourceCode)
: sourceCode,
rawSourceCode: sourceCode,
};
if (isTestFile(relativePath)) {
testSources.push(collectedSource);
continue;
}
runtimeSources.push(collectedSource);
}
}
return {
runtimeSources,
testSources,
};
}
function formatPaths(paths) {
if (paths.length === 0) {
return "无";
}
return paths.map((item) => ` - ${item}`).join("\n");
}
function evaluateImportMonitor(monitor, runtimeSources, testSources) {
const existingTargets = monitor.targets.filter((target) =>
fs.existsSync(path.join(repoRoot, target)),
);
const missingTargets = monitor.targets.filter(
(target) => !fs.existsSync(path.join(repoRoot, target)),
);
const references = runtimeSources
.filter((file) =>
[...file.resolvedImports].some((resolvedPath) =>
monitor.targets.includes(resolvedPath),
),
)
.map((file) => file.relativePath)
.sort();
const testReferences = testSources
.filter((file) =>
[...file.resolvedImports].some((resolvedPath) =>
monitor.targets.includes(resolvedPath),
),
)
.map((file) => file.relativePath)
.sort();
const violations = references.filter(
(relativePath) => !monitor.allowedPaths.includes(relativePath),
);
return {
...monitor,
existingTargets,
missingTargets,
references,
testReferences,
violations,
};
}
function evaluateCommandMonitor(monitor, runtimeSources, testSources) {
const referencesByCommand = new Map();
const testReferencesByCommand = new Map();
for (const command of monitor.commands) {
referencesByCommand.set(
command,
runtimeSources
.filter((file) => file.commands.has(command))
.map((file) => file.relativePath)
.sort(),
);
testReferencesByCommand.set(
command,
testSources
.filter((file) => file.commands.has(command))
.map((file) => file.relativePath)
.sort(),
);
}
const violations = [];
for (const [command, references] of referencesByCommand.entries()) {
for (const relativePath of references) {
if (!monitor.allowedPaths.includes(relativePath)) {
violations.push(`${command} -> ${relativePath}`);
}
}
}
return {
...monitor,
referencesByCommand,
testReferencesByCommand,
violations,
};
}
function evaluateTextMonitor(monitor, runtimeSources, testSources) {
const filteredRuntimeSources = monitor.includePathPrefixes
? runtimeSources.filter((file) =>
monitor.includePathPrefixes.some((prefix) =>
file.relativePath.startsWith(prefix),
),
)
: runtimeSources;
const filteredTestSources = monitor.includePathPrefixes
? testSources.filter((file) =>
monitor.includePathPrefixes.some((prefix) =>
file.relativePath.startsWith(prefix),
),
)
: testSources;
const matchesPattern = (sourceCode) =>
monitor.patterns.some((pattern) => sourceCode.includes(pattern)) ||
(monitor.regexPatterns ?? []).some((pattern) =>
new RegExp(pattern, "m").test(sourceCode),
);
const references = filteredRuntimeSources
.filter((file) => matchesPattern(file.sourceCode))
.map((file) => file.relativePath)
.sort();
const testReferences = filteredTestSources
.filter((file) => matchesPattern(file.rawSourceCode ?? file.sourceCode))
.map((file) => file.relativePath)
.sort();
const violations = references.filter(
(relativePath) => !monitor.allowedPaths.includes(relativePath),
);
return {
...monitor,
references,
testReferences,
violations,
};
}
function countOccurrences(sourceCode, pattern) {
if (!pattern) {
return 0;
}
let count = 0;
let startIndex = 0;
while (true) {
const matchIndex = sourceCode.indexOf(pattern, startIndex);
if (matchIndex === -1) {
return count;
}
count += 1;
startIndex = matchIndex + pattern.length;
}
}
function evaluateTextCountMonitor(monitor, runtimeSources, testSources) {
const filteredRuntimeSources = monitor.includePathPrefixes
? runtimeSources.filter((file) =>
monitor.includePathPrefixes.some((prefix) =>
file.relativePath.startsWith(prefix),
),
)
: runtimeSources;
const filteredTestSources = monitor.includePathPrefixes
? testSources.filter((file) =>
monitor.includePathPrefixes.some((prefix) =>
file.relativePath.startsWith(prefix),
),
)
: testSources;
const runtimeMatches = [];
const testMatches = [];
const violations = [];
for (const file of filteredRuntimeSources) {
const counts = monitor.occurrences
.map((rule) => ({
...rule,
count: countOccurrences(file.sourceCode, rule.pattern),
}))
.filter((rule) => rule.count > 0);
if (counts.length === 0) {
continue;
}
runtimeMatches.push({
relativePath: file.relativePath,
counts,
});
for (const rule of counts) {
if (rule.count > rule.maxCount) {
violations.push(
`${file.relativePath} -> ${rule.pattern} (${rule.count} > ${rule.maxCount})`,
);
}
}
}
for (const file of filteredTestSources) {
const counts = monitor.occurrences
.map((rule) => ({
...rule,
count: countOccurrences(
file.rawSourceCode ?? file.sourceCode,
rule.pattern,
),
}))
.filter((rule) => rule.count > 0);
if (counts.length === 0) {
continue;
}
testMatches.push({
relativePath: file.relativePath,
counts,
});
}
return {
...monitor,
runtimeMatches,
testMatches,
violations,
};
}
function printImportReport(result) {
const status = getImportStatus(result);
console.log(
`- [${status}] ${result.id} (${result.classification}):${result.description}`,
);
console.log(` 目标文件:${result.targets.join(", ")}`);
console.log(` 允许引用:${result.allowedPaths.join(", ") || "无"}`);
if (result.missingTargets.length > 0) {
console.log(` 已删除目标:\n${formatPaths(result.missingTargets)}`);
}
console.log(` 实际引用:\n${formatPaths(result.references)}`);
console.log(` 测试引用:\n${formatPaths(result.testReferences)}`);
if (result.violations.length > 0) {
console.log(` 违规引用:\n${formatPaths(result.violations)}`);
}
}
function printCommandReport(result) {
const status = getCommandStatus(result);
console.log(
`- [${status}] ${result.id} (${result.classification}):${result.description}`,
);
console.log(` 命令:${result.commands.join(", ")}`);
console.log(` 允许引用:${result.allowedPaths.join(", ") || "无"}`);
for (const command of result.commands) {
const references = result.referencesByCommand.get(command) ?? [];
const testReferences = result.testReferencesByCommand.get(command) ?? [];
console.log(` ${command}:\n${formatPaths(references)}`);
console.log(` ${command}(测试):\n${formatPaths(testReferences)}`);
}
if (result.violations.length > 0) {
console.log(` 违规引用:\n${formatPaths(result.violations)}`);
}
}
function printTextReport(result) {
const status = getTextStatus(result);
console.log(
`- [${status}] ${result.id} (${result.classification}):${result.description}`,
);
const keywords = [
...result.patterns,
...(result.regexPatterns ?? []).map((pattern) => `regex:${pattern}`),
];
console.log(` 关键字:${keywords.join(", ")}`);
console.log(` 允许引用:${result.allowedPaths.join(", ") || "无"}`);
console.log(` 实际引用:\n${formatPaths(result.references)}`);
console.log(` 测试引用:\n${formatPaths(result.testReferences)}`);
if (result.violations.length > 0) {
console.log(` 违规引用:\n${formatPaths(result.violations)}`);
}
}
function printTextCountReport(result) {
const status = getTextCountStatus(result);
console.log(
`- [${status}] ${result.id} (${result.classification}):${result.description}`,
);
console.log(
` 次数规则:${result.occurrences
.map((rule) => `${rule.pattern} <= ${rule.maxCount}`)
.join(";")}`,
);
console.log(
` 实际命中:\n${formatPaths(
result.runtimeMatches.map(
(item) =>
`${item.relativePath} -> ${item.counts
.map((rule) => `${rule.pattern} (${rule.count})`)
.join(";")}`,
),
)}`,
);
console.log(
` 测试命中:\n${formatPaths(
result.testMatches.map(
(item) =>
`${item.relativePath} -> ${item.counts
.map((rule) => `${rule.pattern} (${rule.count})`)
.join(";")}`,
),
)}`,
);
if (result.violations.length > 0) {
console.log(` 违规引用:\n${formatPaths(result.violations)}`);
}
}
export function buildLegacySurfaceReport() {
const { runtimeSources, testSources } = collectSources();
const {
runtimeSources: frontendRuntimeTextSources,
testSources: frontendTestTextSources,
} = collectTextSources(sourceRoots, sourceExtensions);
const { runtimeSources: rustRuntimeSources, testSources: rustTestSources } =
collectTextSources(rustSourceRoots, rustSourceExtensions);
const importResults = importSurfaceMonitors.map((monitor) =>
evaluateImportMonitor(monitor, runtimeSources, testSources),
);
const commandResults = commandSurfaceMonitors.map((monitor) =>
evaluateCommandMonitor(monitor, runtimeSources, testSources),
);
const frontendTextResults = frontendTextSurfaceMonitors.map((monitor) =>
evaluateTextMonitor(
monitor,
frontendRuntimeTextSources,
frontendTestTextSources,
),
);
const rustTextResults = rustTextSurfaceMonitors.map((monitor) =>
evaluateTextMonitor(monitor, rustRuntimeSources, rustTestSources),
);
const rustTextCountResults = rustTextCountMonitors.map((monitor) =>
evaluateTextCountMonitor(monitor, rustRuntimeSources, rustTestSources),
);
const summary = buildLegacySurfaceSummary({
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
});
return {
repoRoot,
runtimeSources,
testSources,
rustRuntimeSources,
rustTestSources,
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
...summary,
};
}
export function toSerializableLegacySurfaceReport(report) {
return {
repoRoot: report.repoRoot,
summary: {
runtimeSourceCount: report.runtimeSources.length,
testSourceCount: report.testSources.length,
rustRuntimeSourceCount: report.rustRuntimeSources.length,
rustTestSourceCount: report.rustTestSources.length,
zeroReferenceCandidates: report.zeroReferenceCandidates,
classificationDriftCandidates: report.classificationDriftCandidates,
violations: report.violations,
},
importResults: report.importResults,
commandResults: report.commandResults.map((result) => ({
...result,
referencesByCommand: serializeMapEntries(result.referencesByCommand),
testReferencesByCommand: serializeMapEntries(
result.testReferencesByCommand,
),
})),
frontendTextResults: report.frontendTextResults,
rustTextResults: report.rustTextResults,
rustTextCountResults: report.rustTextCountResults,
};
}
export function printLegacySurfaceReport(report) {
console.log("[lime] legacy surface report");
console.log("");
console.log("## 入口引用");
for (const result of report.importResults) {
printImportReport(result);
}
console.log("");
console.log("## 命令边界");
for (const result of report.commandResults) {
printCommandReport(result);
}
console.log("");
console.log("## 前端护栏");
for (const result of report.frontendTextResults) {
printTextReport(result);
}
console.log("");
console.log("## Rust 护栏");
for (const result of report.rustTextResults) {
printTextReport(result);
}
for (const result of report.rustTextCountResults) {
printTextCountReport(result);
}
console.log("");
console.log("## 摘要");
console.log(`- 扫描文件数:${report.runtimeSources.length}`);
console.log(`- 测试文件数:${report.testSources.length}`);
console.log(`- Rust 扫描文件数:${report.rustRuntimeSources.length}`);
console.log(`- Rust 测试文件数:${report.rustTestSources.length}`);
console.log(`- 零引用候选:${report.zeroReferenceCandidates.length}`);
for (const candidate of report.zeroReferenceCandidates) {
console.log(` - ${candidate}`);
}
console.log(`- 分类漂移候选:${report.classificationDriftCandidates.length}`);
for (const candidate of report.classificationDriftCandidates) {
console.log(` - ${candidate}`);
}
console.log(`- 边界违规:${report.violations.length}`);
for (const violation of report.violations) {
console.log(` - ${violation}`);
}
}
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { toSerializableLegacySurfaceReport } from "./legacy-surface-report-core.mjs";
describe("legacy-surface-report-core", () => {
it("应把命令引用 Map 序列化成普通对象", () => {
const serialized = toSerializableLegacySurfaceReport({
repoRoot: "/tmp/lime",
runtimeSources: [{ relativePath: "src/foo.ts" }],
testSources: [{ relativePath: "src/foo.test.ts" }],
rustRuntimeSources: [{ relativePath: "src-tauri/src/lib.rs" }],
rustTestSources: [],
importResults: [],
commandResults: [
{
id: "agent-runtime",
classification: "current",
description: "runtime command",
commands: ["agent_runtime_submit_turn"],
allowedPaths: ["src/lib/api/agentRuntime.ts"],
referencesByCommand: new Map([
["agent_runtime_submit_turn", ["src/lib/api/agentRuntime.ts"]],
]),
testReferencesByCommand: new Map([
["agent_runtime_submit_turn", ["src/lib/api/agentRuntime.test.ts"]],
]),
violations: [],
},
],
frontendTextResults: [],
rustTextResults: [],
rustTextCountResults: [],
zeroReferenceCandidates: [],
classificationDriftCandidates: [],
violations: [],
});
expect(serialized.summary.runtimeSourceCount).toBe(1);
expect(serialized.commandResults[0].referencesByCommand).toEqual({
agent_runtime_submit_turn: ["src/lib/api/agentRuntime.ts"],
});
expect(serialized.commandResults[0].testReferencesByCommand).toEqual({
agent_runtime_submit_turn: ["src/lib/api/agentRuntime.test.ts"],
});
});
});
@@ -0,0 +1,176 @@
function flattenCommandReferences(result) {
return [...result.referencesByCommand.values()].flat();
}
export function getImportStatus(result) {
return result.violations.length > 0
? "违规"
: result.references.length === 0 && result.existingTargets.length === 0
? "已删除"
: result.references.length === 0
? "零引用"
: "受控";
}
export function getCommandStatus(result) {
const uniqueReferences = [
...new Set(flattenCommandReferences(result)),
].sort();
return result.violations.length > 0
? "违规"
: uniqueReferences.length === 0
? "零引用"
: "受控";
}
export function getTextStatus(result) {
return result.violations.length > 0
? "违规"
: result.references.length === 0
? "零引用"
: "受控";
}
export function getTextCountStatus(result) {
return result.violations.length > 0
? "违规"
: result.runtimeMatches.length === 0
? "零引用"
: "受控";
}
export function isStatusClassificationDrift(status, classification) {
return (
(status === "已删除" || status === "零引用") &&
classification !== "dead-candidate"
);
}
function collectClassificationDriftCandidates({
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
}) {
return [
...importResults
.filter((result) =>
isStatusClassificationDrift(
getImportStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getImportStatus(result)}`,
),
...commandResults
.filter((result) =>
isStatusClassificationDrift(
getCommandStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getCommandStatus(result)}`,
),
...frontendTextResults
.filter((result) =>
isStatusClassificationDrift(
getTextStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextStatus(result)}`,
),
...rustTextResults
.filter((result) =>
isStatusClassificationDrift(
getTextStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextStatus(result)}`,
),
...rustTextCountResults
.filter((result) =>
isStatusClassificationDrift(
getTextCountStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextCountStatus(result)}`,
),
];
}
function collectViolations({
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
}) {
return [
...importResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...commandResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...frontendTextResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...rustTextResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...rustTextCountResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
];
}
export function buildLegacySurfaceSummary({
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
}) {
const zeroReferenceCandidates = importResults
.filter(
(result) =>
result.references.length === 0 && result.existingTargets.length > 0,
)
.map((result) => `${result.id} (${result.description})`);
return {
zeroReferenceCandidates,
classificationDriftCandidates: collectClassificationDriftCandidates({
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
}),
violations: collectViolations({
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
}),
};
}
export function serializeMapEntries(map) {
return Object.fromEntries(map.entries());
}
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import {
buildLegacySurfaceSummary,
getCommandStatus,
getImportStatus,
getTextCountStatus,
getTextStatus,
} from "./legacy-surface-report-summary.mjs";
describe("legacy-surface-report-summary", () => {
it("应正确判定各类 surface 状态", () => {
expect(
getImportStatus({
violations: [],
references: [],
existingTargets: [],
}),
).toBe("已删除");
expect(
getCommandStatus({
violations: [],
referencesByCommand: new Map([["cmd", []]]),
}),
).toBe("零引用");
expect(
getTextStatus({
violations: [],
references: ["src/foo.ts"],
}),
).toBe("受控");
expect(
getTextCountStatus({
violations: [],
runtimeMatches: [],
}),
).toBe("零引用");
});
it("应汇总零引用候选、分类漂移与违规", () => {
const summary = buildLegacySurfaceSummary({
importResults: [
{
id: "import-monitor",
classification: "compat",
description: "import drift",
references: [],
existingTargets: ["src/legacy.ts"],
violations: [],
},
],
commandResults: [
{
id: "command-monitor",
classification: "current",
referencesByCommand: new Map([["legacy_cmd", ["src/foo.ts"]]]),
violations: ["legacy_cmd -> src/foo.ts"],
},
],
frontendTextResults: [
{
id: "frontend-monitor",
classification: "deprecated",
references: [],
violations: [],
},
],
rustTextResults: [],
rustTextCountResults: [],
});
expect(summary.zeroReferenceCandidates).toEqual([
"import-monitor (import drift)",
]);
expect(summary.classificationDriftCandidates).toEqual([
"import-monitor -> compat / 零引用",
"frontend-monitor -> deprecated / 零引用",
]);
expect(summary.violations).toEqual([
"command-monitor -> legacy_cmd -> src/foo.ts",
]);
});
});
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import {
buildGeneratedSlopReport,
renderGeneratedSlopMarkdown,
renderGeneratedSlopText,
} from "./lib/generated-slop-report-core.mjs";
import { buildLiveDocFreshnessReport } from "./check-doc-freshness.mjs";
import {
buildLegacySurfaceReport,
toSerializableLegacySurfaceReport,
} from "./report-legacy-surfaces.mjs";
const TREND_REPORT_PATH = "scripts/harness-eval-trend-report.mjs";
export {
buildGeneratedSlopReport,
renderGeneratedSlopMarkdown,
renderGeneratedSlopText,
} from "./lib/generated-slop-report-core.mjs";
function parseArgs(argv) {
const result = {
format: "text",
help: false,
docFreshnessInput: "",
legacyInput: "",
outputJson: "",
outputMarkdown: "",
trendHistoryDir: "",
trendInput: "",
workspaceRoot: process.cwd(),
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--trend-input" && argv[index + 1]) {
result.trendInput = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--doc-freshness-input" && argv[index + 1]) {
result.docFreshnessInput = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--legacy-input" && argv[index + 1]) {
result.legacyInput = String(argv[index + 1]).trim();
index += 1;
continue;
}
if (arg === "--trend-history-dir" && argv[index + 1]) {
result.trendHistoryDir = 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 === "--help" || arg === "-h") {
result.help = true;
}
}
return result;
}
function printHelp() {
console.log(`
Lime Harness Cleanup / Slop Report
用法:
node scripts/report-generated-slop.mjs
node scripts/report-generated-slop.mjs --trend-input "./tmp/harness-eval-trend.json"
node scripts/report-generated-slop.mjs --doc-freshness-input "./tmp/doc-freshness.json"
node scripts/report-generated-slop.mjs --legacy-input "./tmp/legacy-surface-report.json"
node scripts/report-generated-slop.mjs --trend-history-dir "./artifacts/history"
node scripts/report-generated-slop.mjs --output-json "./tmp/harness-cleanup-report.json" --output-markdown "./tmp/harness-cleanup-report.md"
选项:
--trend-input PATH 显式指定 harness eval trend JSON
--doc-freshness-input PATH 显式指定 doc freshness JSON
--legacy-input PATH 显式指定 governance / legacy surface report JSON
--trend-history-dir PATH 透传给 trend report,用历史 summary 生成趋势
--workspace-root PATH 未提供 trend-input 时,用该工作区生成当前 trend 报告
--format FMT 标准输出格式:text | json | markdown
--output-json PATH 将 cleanup/slop 报告 JSON 写入指定路径
--output-markdown PATH 将 cleanup/slop 报告 Markdown 写入指定路径
-h, --help 显示帮助
`);
}
function resolvePath(baseDir, targetPath) {
return path.resolve(baseDir, targetPath);
}
function ensureParentDirectory(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function detectDefaultTrendHistoryDir(repoRoot) {
const candidate = resolvePath(repoRoot, "./artifacts/history");
if (!fs.existsSync(candidate)) {
return "";
}
const hasJson = fs
.readdirSync(candidate, { withFileTypes: true })
.some((entry) => entry.isFile() && entry.name.endsWith(".json"));
return hasJson ? candidate : "";
}
function buildTrendReport(repoRoot, options) {
if (options.trendInput) {
const resolvedPath = resolvePath(repoRoot, options.trendInput);
return {
report: readJsonFile(resolvedPath),
source: {
kind: "input-json",
path: resolvedPath,
},
};
}
const nodeCommand = process.execPath;
const trendScriptPath = resolvePath(repoRoot, TREND_REPORT_PATH);
const args = [trendScriptPath, "--format", "json"];
const effectiveHistoryDir =
options.trendHistoryDir || detectDefaultTrendHistoryDir(repoRoot);
if (effectiveHistoryDir) {
args.push("--history-dir", effectiveHistoryDir);
}
if (options.workspaceRoot) {
args.push("--workspace-root", options.workspaceRoot);
}
const output = execFileSync(nodeCommand, args, {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
});
return {
report: JSON.parse(output),
source: {
kind: effectiveHistoryDir ? "generated-history" : "generated-current",
path: effectiveHistoryDir
? resolvePath(repoRoot, effectiveHistoryDir)
: "",
},
};
}
function buildGovernanceReport(repoRoot, options) {
if (options.legacyInput) {
const resolvedPath = resolvePath(repoRoot, options.legacyInput);
return {
report: readJsonFile(resolvedPath),
source: {
kind: "input-json",
path: resolvedPath,
},
};
}
return {
report: toSerializableLegacySurfaceReport(buildLegacySurfaceReport()),
source: {
kind: "live-scan",
path: "",
},
};
}
function buildDocFreshnessReport(repoRoot, options) {
if (options.docFreshnessInput) {
const resolvedPath = resolvePath(repoRoot, options.docFreshnessInput);
return {
report: readJsonFile(resolvedPath),
source: {
kind: "input-json",
path: resolvedPath,
},
};
}
return {
report: buildLiveDocFreshnessReport(repoRoot),
source: {
kind: "live-scan",
path: "",
},
};
}
function renderOutput(report, format) {
if (format === "json") {
return `${JSON.stringify(report, null, 2)}\n`;
}
if (format === "markdown") {
return renderGeneratedSlopMarkdown(report);
}
return renderGeneratedSlopText(report);
}
function runGeneratedSlopReportCli() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
const repoRoot = process.cwd();
const trendResult = buildTrendReport(repoRoot, options);
const docFreshnessResult = buildDocFreshnessReport(repoRoot, options);
const governanceResult = buildGovernanceReport(repoRoot, options);
const report = buildGeneratedSlopReport({
repoRoot,
trendReport: trendResult.report,
docFreshnessReport: docFreshnessResult.report,
governanceReport: governanceResult.report,
sources: {
trend: trendResult.source,
docFreshness: docFreshnessResult.source,
governance: governanceResult.source,
},
});
if (options.outputJson) {
const targetPath = resolvePath(repoRoot, options.outputJson);
ensureParentDirectory(targetPath);
fs.writeFileSync(targetPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
console.log(`[lime] cleanup/slop report JSON: ${targetPath}`);
}
if (options.outputMarkdown) {
const targetPath = resolvePath(repoRoot, options.outputMarkdown);
ensureParentDirectory(targetPath);
fs.writeFileSync(targetPath, renderGeneratedSlopMarkdown(report), "utf8");
console.log(`[lime] cleanup/slop report Markdown: ${targetPath}`);
}
process.stdout.write(renderOutput(report, options.format));
}
const isMainModule =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMainModule) {
runGeneratedSlopReportCli();
}
File diff suppressed because it is too large Load Diff