import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import type { ForensicReport } from "../../web/lib/diagnostics-types.ts"; import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"; import { buildSubprocessPrefixArgs, resolveSubprocessModule, } from "./ts-subprocess-flags.ts"; const FORENSICS_MAX_BUFFER = 2 * 1024 * 1024; const FORENSICS_MODULE_ENV = "SF_FORENSICS_MODULE"; function resolveTsLoaderPath(packageRoot: string): string { return join( packageRoot, "src", "resources", "extensions", "sf", "tests", "resolve-ts.mjs", ); } /** * Loads forensic report data via a child process. Converts the full upstream * ForensicReport into a browser-safe subset: deep ExecutionTrace objects are * replaced with trace counts and simplified entries, MetricsLedger is flattened * to summary totals, and doctorIssues is replaced with a count (doctor panel * has its own dedicated API route). */ export async function collectForensicsData( projectCwdOverride?: string, ): Promise { const config = resolveBridgeRuntimeConfig(undefined, projectCwdOverride); const { packageRoot, projectCwd } = config; const resolveTsLoader = resolveTsLoaderPath(packageRoot); const moduleResolution = resolveSubprocessModule( packageRoot, "resources/extensions/sf/forensics.ts", ); const forensicsModulePath = moduleResolution.modulePath; if ( !moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(forensicsModulePath)) ) { throw new Error( `forensics data provider not found; checked=${resolveTsLoader},${forensicsModulePath}`, ); } if (moduleResolution.useCompiledJs && !existsSync(forensicsModulePath)) { throw new Error( `forensics data provider not found; checked=${forensicsModulePath}`, ); } // The child script loads the upstream module, calls buildForensicReport(), // simplifies the output for browser consumption, and writes JSON to stdout. const script = [ 'const { pathToFileURL } = await import("node:url");', `const mod = await import(pathToFileURL(process.env.${FORENSICS_MODULE_ENV}).href);`, `const report = await mod.buildForensicReport(process.env.SF_FORENSICS_BASE);`, // Simplify unitTraces: strip deep ExecutionTrace, keep file/unitType/unitId/seq/mtime "const unitTraces = (report.unitTraces || []).map(t => ({", " file: t.file, unitType: t.unitType, unitId: t.unitId, seq: t.seq, mtime: t.mtime,", "}));", // Flatten metrics to summary "let metrics = null;", "if (report.metrics && report.metrics.units) {", " const units = report.metrics.units;", " const totalCost = units.reduce((s, u) => s + u.cost, 0);", " const totalDuration = units.reduce((s, u) => s + (u.finishedAt - u.startedAt), 0);", " metrics = { totalUnits: units.length, totalCost, totalDuration };", "}", "const result = {", " sfVersion: report.sfVersion,", " timestamp: report.timestamp,", " basePath: report.basePath,", " activeMilestone: report.activeMilestone,", " activeSlice: report.activeSlice,", " anomalies: report.anomalies,", " recentUnits: report.recentUnits,", " crashLock: report.crashLock,", " doctorIssueCount: (report.doctorIssues || []).length,", " unitTraceCount: unitTraces.length,", " unitTraces,", " completedKeyCount: (report.completedKeys || []).length,", " metrics,", " journalSummary: report.journalSummary || null,", " activityLogMeta: report.activityLogMeta || null,", "};", "process.stdout.write(JSON.stringify(result));", ].join(" "); const prefixArgs = buildSubprocessPrefixArgs( packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href, ); return await new Promise((resolveResult, reject) => { execFile( process.execPath, [...prefixArgs, "--eval", script], { cwd: packageRoot, env: { ...process.env, [FORENSICS_MODULE_ENV]: forensicsModulePath, SF_FORENSICS_BASE: projectCwd, }, maxBuffer: FORENSICS_MAX_BUFFER, windowsHide: true, }, (error, stdout, stderr) => { if (error) { reject( new Error( `forensics data subprocess failed: ${stderr || error.message}`, ), ); return; } try { resolveResult(JSON.parse(stdout) as ForensicReport); } catch (parseError) { reject( new Error( `forensics data subprocess returned invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, ), ); } }, ); }); }