fix: 修正 CaseRun 模块审查问题

This commit is contained in:
root
2026-07-16 14:35:52 +02:00
parent 517110572d
commit 8a84e6d6a9
4 changed files with 134 additions and 34 deletions
+73 -17
View File
@@ -5,24 +5,80 @@ import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { renderTraceRowsMarkdown, traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
import { agentTerminalEvidence, commandsFromToolCallSummary, hwpodCommandKind, mentionsHwpod, summarizeAgentTrace } from "./hwlab-caserun-evidence.ts";
import {
agentTerminalEvidence,
arrayOfObjects,
clean,
clipText,
commandsFromToolCallSummary,
compactObject,
hwpodCommandKind,
mentionsHwpod,
numberOption,
parseJsonMaybe,
summarizeAgentTrace,
text,
uniqueStrings,
} from "./hwlab-caserun-evidence.ts";
import type { ArchivedAgentTraceSnapshot, AgentTraceCommand, AgentTraceStage, CaseContext, CaseReadableAgentArchive, CaseRegistryArchive, CaseRegistryArchiveFile, PreparedCaseRun } from "./hwlab-caserun-shared.ts";
const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"; const MAX_AGENT_TRACE_COMMANDS = 30; const HWLAB_API_KEY_PREFIX = "hwl_live_";
function text(value: unknown) { return String(value ?? "").trim(); }
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
function clean(value: any) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }
function compactObject(value: any) { const json = JSON.stringify(value ?? null); return json.length <= 4000 ? value : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) }; }
function sha256Buffer(value: Buffer) { return createHash("sha256").update(value).digest("hex"); }
function parseJsonMaybe(value: unknown) { if (value && typeof value === "object") return value as any; try { return JSON.parse(String(value ?? "").trim()); } catch { return null; } }
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw Object.assign(new Error(`${field} is required`), { code: "missing_required_value", details: { field } }); return result; }
function clipText(value: unknown, maxBytes = 2000) { const raw = String(value ?? ""); const buffer = Buffer.from(raw, "utf8"); return buffer.length <= maxBytes ? raw : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`; }
function arrayOfObjects(value: unknown): any[] { return Array.isArray(value) ? value.filter((item) => item && typeof item === "object" && !Array.isArray(item)) : []; }
function uniqueStrings(values: string[]) { return Array.from(new Set(values.map(text).filter(Boolean))); }
async function readJsonIfExists(file: string) { try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } }
async function readTextIfExists(file: string) { try { return await readFile(file, "utf8"); } catch { return ""; } }
async function fileExists(file: string) { try { await stat(file); return true; } catch { return false; } }
async function writeJson(file: string, value: any) { await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8"); }
async function runProcess(command: string[], cwd: string, env: Record<string, string | undefined>) { const proc = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe" }); const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]); return { command, stdout, stderr, exitCode }; }
const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows";
const MAX_AGENT_TRACE_COMMANDS = 30;
const HWLAB_API_KEY_PREFIX = "hwl_live_";
function sha256Buffer(value: Buffer) {
return createHash("sha256").update(value).digest("hex");
}
function requiredText(value: unknown, field: string) {
const result = text(value);
if (!result) {
throw Object.assign(new Error(`${field} is required`), {
code: "missing_required_value",
details: { field },
});
}
return result;
}
async function readJsonIfExists(file: string) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch {
return null;
}
}
async function readTextIfExists(file: string) {
try {
return await readFile(file, "utf8");
} catch {
return "";
}
}
async function fileExists(file: string) {
try {
await stat(file);
return true;
} catch {
return false;
}
}
async function writeJson(file: string, value: any) {
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
async function runProcess(command: string[], cwd: string, env: Record<string, string | undefined>) {
const proc = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
return { command, stdout, stderr, exitCode };
}
export function agentTraceLookupForRun(run: any, evidence?: any) {
const agent = run.agent ?? evidence?.agent;
const traceId = text(agent?.traceId ?? evidence?.agentTrace?.traceId ?? run.agentTrace?.traceId);
+40 -7
View File
@@ -112,11 +112,44 @@ export function firstTextOption(...values: any[]) { return values.map(text).find
export function firstNumberOption(...values: any[]) { for (const value of values) { const number = numberOption(value); if (number !== undefined) return number; } return undefined; }
function countedRecord(value: any, fallbacks: Array<string | undefined>) { if (value && typeof value === "object" && !Array.isArray(value)) return Object.fromEntries(Object.entries(value).map(([key, count]) => [key, Number(count) || 0])); return fallbacks.filter(Boolean).reduce<Record<string, number>>((result, key) => ({ ...result, [key as string]: (result[key as string] ?? 0) + 1 }), {}); }
function sumRecordValues(value: Record<string, number>) { return Object.values(value).reduce((sum, count) => sum + count, 0); }
function compactObject(value: any) { const json = JSON.stringify(value ?? null); return json.length <= 4000 ? value : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) }; }
function clipText(value: unknown, maxBytes: number) { const raw = String(value ?? ""); const buffer = Buffer.from(raw, "utf8"); return buffer.length <= maxBytes ? raw : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`; }
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
function text(value: unknown) { return String(value ?? "").trim(); }
function clean<T extends Record<string, unknown>>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)) as T; }
export function compactObject(value: any) {
const json = JSON.stringify(value ?? null);
return json.length <= 4000
? value
: { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) };
}
export function clipText(value: unknown, maxBytes = 2000) {
const raw = String(value ?? "");
const buffer = Buffer.from(raw, "utf8");
return buffer.length <= maxBytes
? raw
: `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`;
}
export function numberOption(value: unknown) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isFinite(parsed) ? parsed : undefined;
}
export function parseJsonMaybe(value: unknown) {
if (value && typeof value === "object") return value as any;
try {
return JSON.parse(String(value ?? "").trim());
} catch {
return null;
}
}
export function text(value: unknown) {
return String(value ?? "").trim();
}
export function clean<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(
Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null),
) as T;
}
type AgentTraceCommand = { source: string; seq?: number; rowId?: string; header?: string; toolName?: string; status?: string; command?: string; normalizedCommand?: string; bodyPreview?: string; exitCode?: number };
const MAX_AGENT_TRACE_COMMANDS = 30;
@@ -231,7 +264,7 @@ function selectRepresentativeHwpodCommands(commands: AgentTraceCommand[]) {
});
}
function arrayOfObjects(value: unknown): Record<string, unknown>[] {
export function arrayOfObjects(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item) => item && typeof item === "object" && !Array.isArray(item)) as Record<string, unknown>[] : [];
}
@@ -271,6 +304,6 @@ function unquoteShellCommand(value: string) {
return value.replace(/^\/bin\/sh\s+-lc\s+/u, "").replace(/^['"]|['"]$/gu, "").trim();
}
function uniqueStrings(values: string[]) {
export function uniqueStrings(values: string[]) {
return Array.from(new Set(values.map((value) => text(value)).filter(Boolean)));
}
+1
View File
@@ -3,6 +3,7 @@
import type { AgentDiffCollectionSummary } from "./hwlab-caserun-diff.ts";
import type { AgentToolCallSummary } from "./hwlab-caserun-evidence.ts";
import type { TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
export type EnvLike = Record<string, string | undefined>;
export type ParsedArgs = Record<string, unknown> & { _: string[] };
+20 -10
View File
@@ -5,17 +5,27 @@ import { createHash, randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { readHwpodSpec } from "./hwpod-harness-lib.ts";
import { firstNumberOption } from "./hwlab-caserun-evidence.ts";
import {
clean,
clipText,
compactObject,
firstNumberOption,
numberOption,
parseJsonMaybe,
text,
} from "./hwlab-caserun-evidence.ts";
import type { CaseContext, PreparedCaseRun, PreparedSubjectRun, SourceRootSnapshot } from "./hwlab-caserun-shared.ts";
const DEFAULT_POLL_INTERVAL_MS = 1000; const DEFAULT_JOB_TIMEOUT_MS = 50000; const MAX_JOB_TIMEOUT_MS = 300000;
function text(value: unknown) { return String(value ?? "").trim(); }
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
function clean(value: any) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }
function compactObject(value: any) { const json = JSON.stringify(value ?? null); return json.length <= 4000 ? value : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) }; }
function clipText(value: unknown, maxBytes = 2000) { const raw = String(value ?? ""); const buffer = Buffer.from(raw, "utf8"); return buffer.length <= maxBytes ? raw : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`; }
function parseJsonMaybe(value: unknown) { if (value && typeof value === "object") return value as any; try { return JSON.parse(String(value ?? "").trim()); } catch { return null; } }
function cliError(code: string, message: string, details: any = {}) { return Object.assign(new Error(message), { code, details }); }
function slug(value: string) { return value.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "case"; }
const DEFAULT_POLL_INTERVAL_MS = 1000;
const DEFAULT_JOB_TIMEOUT_MS = 50000;
const MAX_JOB_TIMEOUT_MS = 300000;
function cliError(code: string, message: string, details: any = {}) {
return Object.assign(new Error(message), { code, details });
}
function slug(value: string) {
return value.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "case";
}
export async function prepareSubjectWorktree(context: CaseContext, input: { caseId: string; runId: string; runDir: string; apiUrl: string; definition: Record<string, unknown>; sourceSpecPath: string }, collectSnapshot: (context: CaseContext, run: PreparedCaseRun, label: string) => Promise<SourceRootSnapshot>) {
const subject = subjectFromDefinition(input.definition);
const worktreePath = subjectWorktreePath(subject.repoLocalPath, input.runId);