// SPEC: PJ2026-01060703 CI/CD branch follower rendering. // Responsibility: machine and human output rendering for cicd branch-follower. import type { RenderedCliResult } from "./output"; import type { ParsedOptions } from "./cicd-types"; import { renderDebugStepHuman } from "./cicd-debug"; import { renderDrillDownHuman } from "./cicd-drilldown-render"; export function renderResult(command: string, payload: Record, options: ParsedOptions): RenderedCliResult { const ok = payload.ok !== false; if (options.output === "json") return renderMachine(command, payload, "json", ok); if (options.output === "yaml") return renderMachine(command, payload, "yaml", ok); return rendered(ok, command, renderHuman(command, payload, options)); } export function renderMachine(command: string, value: unknown, mode: "json" | "yaml", ok = true): RenderedCliResult { return rendered(ok, command, mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`, mode === "json" ? "application/json" : "application/yaml"); } function rendered(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult { return { ok, command, renderedText, contentType }; } function renderHuman(command: string, payload: Record, options: ParsedOptions): string { if (command.endsWith(" plan")) return renderPlanHuman(payload); if (command.endsWith(" apply")) return renderApplyHuman(payload); if (command.endsWith(" status")) return renderStatusHuman(payload, options); if (command.endsWith(" run-once")) return renderRunOnceHuman(payload); if (command.endsWith(" debug-step")) return renderDebugStepHuman(payload); if (command.endsWith(" cleanup-state")) return renderCleanupStateHuman(payload); if (command.endsWith(" events") || command.endsWith(" logs") || command.endsWith(" taskrun") || command.endsWith(" job") || command.endsWith(" runtime")) return renderDrillDownHuman(payload); return `${JSON.stringify(payload, null, 2)}\n`; } function renderPlanHuman(payload: Record): string { const followers = arrayRecords(payload.followers); const rows = followers.map((item) => { const source = asOptionalRecord(item.source); const target = asOptionalRecord(item.target); const budgets = asOptionalRecord(item.budgets); return [ item.id, item.enabled, item.adapter, `${source?.repository ?? "-"}@${source?.branch ?? "-"}`, `${target?.node ?? "-"}/${target?.lane ?? "-"}`, budgets?.endToEndSeconds ?? "-", arrayRecords(item.configRefGraph).length, arrayText(item.closeoutChecks), ]; }); const next = asOptionalRecord(payload.next); return [ `CI/CD BRANCH-FOLLOWER PLAN (${payload.ok === false ? "blocked" : "ok"})`, "", table(["FOLLOWER", "ENABLED", "ADAPTER", "SOURCE", "TARGET", "BUDGET", "REFS", "CHECKS"], rows), "", "SOURCE AUTHORITY", `hostWorktreeAuthority=${payload.hostWorktreeAuthority === true ? "true" : "false"} mode=${asOptionalRecord(payload.sourceAuthority)?.mode ?? "-"} resolver=${asOptionalRecord(payload.sourceAuthority)?.resolver ?? "-"}`, "", "NEXT", `apply: ${next?.apply ?? "-"}`, `status: ${next?.status ?? "-"}`, `dry-run: ${next?.dryRun ?? "-"}`, "", ].join("\n"); } function renderApplyHuman(payload: Record): string { const controller = asOptionalRecord(payload.controller); const command = asOptionalRecord(payload.command); const next = asOptionalRecord(payload.next); return [ `CI/CD BRANCH-FOLLOWER APPLY (${payload.ok === false ? "failed" : payload.dryRun === true ? "dry-run" : "ok"})`, "", table( ["NAMESPACE", "ROUTE", "DEPLOYMENT", "STATE_CM", "LEASE", "HOST_WORKTREE"], [[controller?.namespace ?? "-", controller?.route ?? "-", controller?.deploymentName ?? "-", controller?.stateConfigMapName ?? "-", controller?.leaseName ?? "-", controller?.hostWorktreeMounted === true ? "mounted" : "not-mounted"]], ), "", table(["OBJECTS", "MANIFEST_SHA", "EXIT", "TIMED_OUT"], [[arrayRecords(payload.objects).length, shortSha(stringOrNull(payload.manifestSha256)), command?.exitCode ?? "-", command?.timedOut ?? "-"]]), command?.stderrTail ? `\nSTDERR\n${command.stderrTail}` : "", "", "NEXT", `status: ${next?.status ?? "-"}`, `dry-run: ${next?.dryRun ?? "-"}`, "", ].filter((line) => line !== "").join("\n"); } function renderStatusHuman(payload: Record, _options: ParsedOptions): string { const controller = asOptionalRecord(payload.controller); const followers = arrayRecords(payload.followers); const rows = followers.map((item) => { const source = asOptionalRecord(item.source); const target = asOptionalRecord(item.target); const budgets = asOptionalRecord(item.budgetSource); return [ item.id, item.phase, item.adapter, `${source?.branch ?? "-"}:${shortSha(stringOrNull(source?.observedSha))}`, shortSha(stringOrNull(target?.targetSha)), shortSha(stringOrNull(item.lastTriggeredSha)), shortSha(stringOrNull(item.lastSucceededSha)), item.pipelineRun ?? item.inFlightJob ?? "-", budgets?.endToEndSeconds ?? "-", item.message ?? "-", ]; }); const next = asOptionalRecord(payload.next); const errors = Array.isArray(payload.errors) ? payload.errors : []; const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48); const performanceRows = followers.flatMap(performanceRowsForFollower).slice(0, 24); const evidenceRows = followers.flatMap(evidenceRowsForFollower).slice(0, 48); const reconcileRows = followers.flatMap(reconcileRowsForFollower).slice(0, 48); const rawStateRows = followers.flatMap(rawStateRowsForFollower).slice(0, 24); return [ `CI/CD BRANCH-FOLLOWER STATUS (${payload.ok === false ? "degraded" : "ok"})`, "", table( ["CTRL_NS", "ROUTE", "DEPLOY", "READY", "PODS", "STATE_CM", "LEASE"], [[controller?.namespace ?? "-", controller?.route ?? "-", controller?.deploymentName ?? "-", `${controller?.availableReplicas ?? 0}/${controller?.replicas ?? 0}`, controller?.pods ?? "-", controller?.stateConfigMapPresent === true ? "present" : "missing", controller?.leaseHolder ?? "-"]], ), "", table(["FOLLOWER", "PHASE", "ADAPTER", "OBSERVED", "TARGET", "TRIGGERED", "SUCCEEDED", "IN_FLIGHT", "BUDGET", "MESSAGE"], rows), timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`, performanceRows.length === 0 ? "" : `\nSLOW STAGES\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "SOURCE", "OBJECT"], performanceRows)}`, evidenceRows.length === 0 ? "" : `\nEVIDENCE\n${table(["FOLLOWER", "TYPE", "STATUS", "DETAIL", "OBJECT"], evidenceRows)}`, reconcileRows.length === 0 ? "" : `\nRECONCILE TIMELINE\n${table(["FOLLOWER", "STEP", "STATUS", "SECONDS", "STARTED", "OBJECT"], reconcileRows)}`, rawStateRows.length === 0 ? "" : `\nRAW STATE DIAGNOSTIC\n${table(["FOLLOWER", "STATE_BYTES", "COMMAND", "TIMELINE", "STEPS", "TIMELINE_BYTES", "REASON"], rawStateRows)}`, errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`, "", "NEXT", `live-status: ${next?.liveStatus ?? "-"}`, `dry-run: ${next?.dryRun ?? "-"}`, "", ].filter((line) => line !== "").join("\n"); } function renderRunOnceHuman(payload: Record): string { const followers = arrayRecords(payload.followers); const stateWrites = arrayRecords(payload.stateWrites); const rows = followers.map((item) => { const source = asOptionalRecord(item.source); const target = asOptionalRecord(item.target); return [ item.id, item.phase, `${source?.branch ?? "-"}:${shortSha(stringOrNull(source?.observedSha))}`, shortSha(stringOrNull(target?.targetSha)), shortSha(stringOrNull(item.lastTriggeredSha)), item.inFlightJob ?? "-", item.decision ?? "-", ]; }); const next = asOptionalRecord(payload.next); const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48); const reconcileRows = reconcileRowsFromRunOnce(payload, followers).slice(0, 48); const writeRows = stateWrites.map((item) => [ item.follower, item.ok === true ? "ok" : "failed", item.beforeResourceVersion ?? "-", item.afterResourceVersion ?? "-", item.preservedTiming === true ? "yes" : "no", item.exitCode ?? "-", item.message ?? "-", ]); return [ `CI/CD BRANCH-FOLLOWER RUN-ONCE (${payload.ok === false ? "blocked" : payload.dryRun === true ? "dry-run" : "ok"})`, "", table(["FOLLOWER", "PHASE", "OBSERVED", "TARGET", "TRIGGERED", "IN_FLIGHT", "DECISION"], rows), timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`, reconcileRows.length === 0 ? "" : `\nRECONCILE TIMELINE\n${table(["FOLLOWER", "STEP", "STATUS", "SECONDS", "STARTED", "OBJECT"], reconcileRows)}`, writeRows.length === 0 ? "" : `\nSTATE WRITES\n${table(["FOLLOWER", "STATUS", "BEFORE_RV", "AFTER_RV", "PRESERVED", "EXIT", "MESSAGE"], writeRows)}`, "", "NEXT", `status: ${next?.status ?? "-"}`, `live-status: ${next?.liveStatus ?? "-"}`, "", ].join("\n"); } function renderCleanupStateHuman(payload: Record): string { const controller = asOptionalRecord(payload.controller); const command = asOptionalRecord(payload.command); const followers = arrayRecords(payload.followers); const next = asOptionalRecord(payload.next); const rows = followers.map((item) => [ item.id, item.statePresent === true ? "present" : "missing", item.cleanup ?? "-", ]); return [ `CI/CD BRANCH-FOLLOWER CLEANUP-STATE (${payload.ok === false ? "failed" : payload.dryRun === true ? "dry-run" : "ok"})`, "", table( ["NAMESPACE", "ROUTE", "STATE_CM", "STATE_CM_PRESENT"], [[controller?.namespace ?? "-", controller?.route ?? "-", controller?.stateConfigMapName ?? "-", payload.stateConfigMapPresent === true ? "present" : "missing"]], ), "", table(["FOLLOWER", "STATE", "CLEANUP"], rows), command === null ? "" : `\nPATCH\nexit=${command.exitCode ?? "-"} timedOut=${command.timedOut ?? "-"}`, "", "NEXT", `status: ${next?.status ?? "-"}`, `run-once: ${next?.runOnce ?? "-"}`, "", ].filter((line) => line !== "").join("\n"); } function timingRowsForFollower(item: Record): unknown[][] { const timings = asOptionalRecord(item.timings); if (timings === null) return []; const budget = numberOrNull(timings.budgetSeconds); const rows: unknown[][] = [[ item.id, "total", stringOrNull(timings.totalStatus) ?? "unknown", formatSeconds(numberOrNull(timings.totalSeconds)), formatSeconds(budget), [stringOrNull(timings.totalSource), shortSha(stringOrNull(timings.sourceCommit))].filter((value) => value !== null && value !== "-").join(":") || "-", ]]; for (const stage of arrayRecords(timings.stages)) { rows.push([ item.id, stage.stage, stage.status, formatSeconds(numberOrNull(stage.seconds)), formatSeconds(numberOrNull(stage.budgetSeconds)), stringOrNull(stage.object) ?? "-", ]); } return rows; } function performanceRowsForFollower(item: Record): unknown[][] { const performance = asOptionalRecord(item.performance); if (performance === null) return []; return arrayRecords(performance.slowStages).map((stage) => [ item.id, stage.stage ?? "-", stage.status ?? "-", formatSeconds(numberOrNull(stage.seconds)), stringOrNull(stage.source) ?? "-", stringOrNull(stage.object) ?? "-", ]); } function reconcileRowsFromRunOnce(payload: Record, followers: Record[]): unknown[][] { const timeline = asOptionalRecord(payload.reconcileTimeline); if (timeline !== null) return reconcileRowsForTimeline(timeline, null); return followers.flatMap(reconcileRowsForFollower); } function evidenceRowsForFollower(item: Record): unknown[][] { const evidence = asOptionalRecord(item.evidence); if (evidence === null) return []; const rows: unknown[][] = []; rows.push([item.id, "pipelineRef", "observed", stringOrNull(evidence.pipelineRunRefName) ?? "-", stringOrNull(item.pipelineRun) ?? "-"]); const pipeline = asOptionalRecord(evidence.pipeline); if (pipeline !== null) { const runtimeReady = asOptionalRecord(asOptionalRecord(pipeline.spec)?.runtimeReadyTask); const when = arrayRecords(runtimeReady?.when)[0]; rows.push([ item.id, "pipelineSpec", runtimeReady?.present === true ? "runtime-ready-present" : "runtime-ready-absent", when === undefined ? "-" : `${stringOrNull(when.input) ?? "-"} ${stringOrNull(when.operator) ?? "-"} ${arrayText(when.values) || "-"}`, stringOrNull(asOptionalRecord(pipeline.metadata)?.name) ?? "-", ]); } const refresh = asOptionalRecord(evidence.refresh); rows.push([ item.id, "refresh", refresh === null ? "missing" : stringOrNull(refresh.status) ?? "-", refresh === null ? stringOrNull(evidence.refreshBoundedReason) ?? "-" : `${shortSha(stringOrNull(refresh.sourceCommit))}/${boolMatch(refresh.pipelineRefMatches)}/${boolMatch(refresh.pipelineSpecMatches)}`, refresh === null ? "-" : stringOrNull(refresh.pipeline) ?? "-", ]); const refreshRender = asOptionalRecord(refresh?.render); const refreshRenderRuntimeReady = asOptionalRecord(refreshRender?.runtimeReadyTask); if (refreshRender !== null) { rows.push([ item.id, "refresh-render", refreshRenderRuntimeReady?.present === true ? "runtime-ready-present" : refreshRenderRuntimeReady?.present === false ? "runtime-ready-absent" : "-", whenSummary(arrayRecords(refreshRenderRuntimeReady?.when)[0]), stringOrNull(refreshRender.pipelineName) ?? "-", ]); } const refreshApply = asOptionalRecord(refresh?.apply); if (refreshApply !== null) { rows.push([ item.id, "refresh-apply", stringOrNull(refreshApply.resourceVersion) ?? stringOrNull(refreshApply.degradedReason) ?? "-", applyMetadataSummary(refreshApply), stringOrNull(refreshApply.pipelineName) ?? "-", ]); } return rows; } function reconcileRowsForFollower(item: Record): unknown[][] { return reconcileRowsForTimeline(asOptionalRecord(item.reconcileTimeline), stringOrNull(item.id)); } function reconcileRowsForTimeline(timeline: Record | null, fallbackFollower: string | null): unknown[][] { if (timeline === null) return []; const steps = arrayRecords(timeline.steps); if (steps.length === 0 && stringOrNull(timeline.missingReason) !== null) { return [[fallbackFollower ?? "-", "controller-loop", "-", "-", "-", stringOrNull(timeline.missingReason)]]; } return steps.map((step) => [ stringOrNull(step.follower) ?? fallbackFollower ?? "-", step.step ?? "-", step.status ?? "-", formatSeconds(secondsFromMs(numberOrNull(step.elapsedMs))), stringOrNull(step.startedAt) ?? "-", stringOrNull(step.object) ?? stringOrNull(step.pipelineRun) ?? shortSha(stringOrNull(step.observedSha)), ]); } function rawStateRowsForFollower(item: Record): unknown[][] { const diagnostic = asOptionalRecord(item.rawStateDiagnostic); if (diagnostic === null) return []; return [[ item.id ?? "-", diagnostic.valueBytes ?? "-", diagnostic.hasCommand === true ? "yes" : "no", diagnostic.hasReconcileTimeline === true ? "yes" : "no", diagnostic.reconcileTimelineStepCount ?? "-", diagnostic.reconcileTimelineBytes ?? "-", stringOrNull(diagnostic.missingReason) ?? "-", ]]; } function secondsFromMs(value: number | null): number | null { return value === null ? null : Math.round(value / 100) / 10; } function formatSeconds(value: number | null): string { return value === null ? "-" : `${value}s`; } function boolMatch(value: unknown): string { return value === true ? "match" : value === false ? "mismatch" : "-"; } function whenSummary(value: Record | undefined): string { if (value === undefined) return "-"; const values = arrayText(value.values); return `${stringOrNull(value.input) ?? "-"} ${stringOrNull(value.operator) ?? "-"} ${values || "-"}`; } function applyMetadataSummary(value: Record): string { const annotations = asOptionalRecord(value.annotations); const labels = asOptionalRecord(value.labels); const annotation = annotations === null ? "-" : `${firstEntry(annotations)}`; const label = labels === null ? "-" : `${firstEntry(labels)}`; return `ann:${annotation} label:${label}`; } function firstEntry(value: Record): string { const [key, item] = Object.entries(value)[0] ?? []; return key === undefined ? "-" : `${key}=${stringOrNull(item) ?? "-"}`; } function asOptionalRecord(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; } function arrayRecords(value: unknown): Record[] { return Array.isArray(value) ? value.filter((item): item is Record => typeof item === "object" && item !== null && !Array.isArray(item)) : []; } function arrayText(value: unknown): string { return Array.isArray(value) ? value.map(String).join(",") : "-"; } function stringOrNull(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } function numberOrNull(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function shortSha(value: string | null): string { if (value === null) return "-"; return value.length > 12 ? value.slice(0, 12) : value; } function table(headers: readonly string[], rows: readonly (readonly unknown[])[]): string { const normalized = rows.map((row) => headers.map((_, index) => cell(row[index]))); const widths = headers.map((header, index) => Math.max(header.length, ...normalized.map((row) => row[index]?.length ?? 0))); const format = (row: readonly string[]) => row.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ").trimEnd(); return [format(headers), format(headers.map((header) => "-".repeat(header.length))), ...normalized.map(format)].join("\n"); } function cell(value: unknown): string { if (value === null || value === undefined || value === "") return "-"; const text = String(value).replace(/\s+/gu, " "); return text.length > 96 ? `${text.slice(0, 93)}...` : text; }