109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
/**
|
|
* status.ts — readable run-state formatter.
|
|
*
|
|
* {@link formatRunStatus} turns a `RunState` into a plain line list covering
|
|
* the run-level summary and one block per registered check: overall status, the
|
|
* per-phase breakdown, captured findings/changes artifacts, and any errors.
|
|
* It is a *pure* function of state — no disk I/O — so it is trivially
|
|
* unit-testable and deterministic; the in-memory run-state is the single source
|
|
* of truth for progress (the check-runner records findings/changes text on it).
|
|
*
|
|
* `formatRunStatus` is the single helper the `/pygienium-status` command uses;
|
|
* keeping it here (out of `commands.ts`) lets `commands.ts` stay a thin binder.
|
|
*
|
|
* @module pygienium/status
|
|
*/
|
|
|
|
import type { CheckRun, RunState } from "./run-state.js";
|
|
|
|
const PHASE_ORDER = ["recon", "analysis", "fix", "verify", "cleanup"] as const;
|
|
|
|
function toISO(ms: number | undefined): string {
|
|
return ms == null ? "—" : new Date(ms).toISOString();
|
|
}
|
|
|
|
function short(status: string): string {
|
|
return status[0]?.toUpperCase() ?? "?";
|
|
}
|
|
|
|
/** Count non-empty lines in captured findings/changes text. */
|
|
function lineCount(text: string | undefined): number {
|
|
if (!text) return 0;
|
|
const count = text.split(/\r?\n/).filter((l) => l.trim().length > 0).length;
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Format a single check block (without a trailing separator) — exposed so tests
|
|
* and the status command share one rendering path.
|
|
*/
|
|
export function formatCheckBlock(check: CheckRun, indent = " "): string[] {
|
|
const lines: string[] = [];
|
|
const flag = check.fix ? " (--fix)" : "";
|
|
lines.push(`${indent}${check.name} — ${check.status}${flag}`);
|
|
|
|
const phaseSummary = check.phases
|
|
.map((p) => `${p.id}:${p.status.startsWith("in_progress") ? "…" : short(p.status)}`)
|
|
.join(" ");
|
|
if (phaseSummary) lines.push(`${indent} phases: ${phaseSummary}`);
|
|
|
|
const findingsLines = lineCount(check.findings);
|
|
if (findingsLines > 0) {
|
|
lines.push(`${indent} findings: ${findingsLines} line(s)`);
|
|
}
|
|
const changesLines = lineCount(check.changes);
|
|
if (changesLines > 0) {
|
|
lines.push(`${indent} changes: ${changesLines} line(s)`);
|
|
}
|
|
|
|
if (check.error) {
|
|
lines.push(`${indent} error: ${check.error}`);
|
|
}
|
|
for (const phase of check.phases) {
|
|
if (phase.status === "failed" && phase.error && phase.error !== check.error) {
|
|
lines.push(`${indent} ${phase.id}: ${phase.error}`);
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Build the `/pygienium-status` line list for a run state. Pure: no disk reads.
|
|
* Layout:
|
|
*
|
|
* pygienium run — <status>
|
|
* started: <iso>
|
|
* updated: <iso>
|
|
* cwd: <cwd>
|
|
* recon: complete|pending
|
|
*
|
|
* checks (N):
|
|
* <name> — <status>
|
|
* phases: recon:✓ analysis:✓ [fix:✓] verify:✓ cleanup:✓
|
|
* findings: <N> line(s)
|
|
* changes: <N> line(s)
|
|
* error: <msg>
|
|
*/
|
|
export function formatRunStatus(state: RunState): string[] {
|
|
const checks = Object.values(state.checks);
|
|
const lines: string[] = [];
|
|
|
|
lines.push(`pygienium run — ${state.status}`);
|
|
lines.push(` started: ${toISO(state.startedAt)}`);
|
|
lines.push(` updated: ${toISO(state.updatedAt)}`);
|
|
lines.push(` cwd: ${state.cwd}`);
|
|
const reconLabel = state.recon.complete ? "complete" : "pending";
|
|
const reconTime = state.recon.finishedAt ? ` (${toISO(state.recon.finishedAt)})` : "";
|
|
lines.push(` recon: ${reconLabel}${reconTime}`);
|
|
lines.push("");
|
|
|
|
lines.push(` checks (${checks.length}):`);
|
|
if (checks.length === 0) {
|
|
lines.push(" (none registered in this run state)");
|
|
}
|
|
for (const check of checks) {
|
|
lines.push(...formatCheckBlock(check));
|
|
}
|
|
return lines;
|
|
}
|