// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p16-cicd-source-snapshot. // Responsibility: shared types, config helpers and text render utilities for web-probe sentinel CI/CD. import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery"; import type { CommandResult } from "./command"; import { repoRoot, rootPath } from "./config"; import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes"; import type { RenderedCliResult } from "./output"; export type WebProbeSentinelConfigAction = "plan" | "status"; export type WebProbeSentinelImageAction = "status" | "build"; export type WebProbeSentinelControlPlaneAction = "plan" | "apply" | "status" | "trigger-current"; export type WebProbeSentinelPublishAction = "publish-current"; export type WebProbeSentinelMaintenanceAction = "status" | "start" | "stop"; export type WebProbeSentinelDashboardAction = "verify" | "screenshot" | "trigger"; export type WebProbeSentinelReportView = "summary" | "turn-summary" | "findings" | "trace-frame" | "auth-session-switch-summary"; export type WebProbeSentinelOptions = | { readonly kind: "config"; readonly action: WebProbeSentinelConfigAction; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly dryRun: boolean; } | { readonly kind: "image"; readonly action: WebProbeSentinelImageAction; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; } | { readonly kind: "control-plane"; readonly action: WebProbeSentinelControlPlaneAction; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; readonly rerun: boolean; } | { readonly kind: "publish"; readonly action: WebProbeSentinelPublishAction; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; readonly rerun: boolean; } | { readonly kind: "maintenance"; readonly action: WebProbeSentinelMaintenanceAction; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; readonly releaseId: string | null; readonly reason: string | null; readonly quickVerify: boolean; } | { readonly kind: "validate"; readonly action: "validate"; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; readonly quickVerify: boolean; } | { readonly kind: "report"; readonly action: "report"; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly view: WebProbeSentinelReportView; readonly runId: string | null; readonly latest: boolean; readonly traceId: string | null; readonly sampleSeq: number | null; readonly raw: boolean; readonly full: boolean; readonly timeoutSeconds: number; } | { readonly kind: "dashboard"; readonly action: WebProbeSentinelDashboardAction; readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly viewport: string; readonly localDir: string; readonly name: string | null; readonly runId: string | null; readonly timeoutMs: number; readonly waitTimeoutMs: number; readonly timeoutSeconds: number; readonly commandTimeoutSeconds: number; readonly fullPage: boolean; readonly raw: boolean; }; export interface SentinelCicdState { readonly spec: HwlabRuntimeLaneSpec; readonly sentinelId: string; readonly configRefs: Record; readonly configReady: boolean; readonly runtime: Record; readonly cicd: Record; readonly scenarios: unknown; readonly publicExposure: Record; readonly secrets: Record; readonly controlPlaneTarget: Record; readonly controlPlaneNode: Record; readonly sourceHead: SourceHead; readonly image: SentinelImagePlan; readonly manifests: readonly Record[]; readonly manifestSha256: string; readonly valuesRedacted: true; } export interface SourceHead { readonly ok: boolean; readonly repository: string; readonly branch: string; readonly commit: string | null; readonly stageRef: string | null; readonly mirrorCommit: string | null; readonly sourceAuthority: "git-mirror-cache" | "git-mirror-snapshot"; readonly latestDrift: boolean; readonly result: CompactCommandResult; } export interface SentinelImagePlan { readonly repository: string; readonly tag: string; readonly ref: string; readonly digestRef: string | null; readonly baseImage: string; readonly buildContext: string; readonly entrypoint: string; readonly dockerfileSha256: string; readonly dockerfilePreview: string; readonly monitorWeb: Record; } export interface SentinelObservedStatus { readonly sourceMirror: Record; readonly registry: Record; readonly gitMirror: Record; readonly gitops: Record; readonly argo: Record; readonly runtime: Record; readonly cadence: Record; readonly wait?: Record; } export interface SentinelObservedExpectation { readonly gitopsRevision: string | null; readonly runtimeImage: string | null; } export interface SentinelRemoteJobResult { readonly ok: boolean; readonly phase: string; readonly resourceKind?: "Job" | "PipelineRun"; readonly jobName: string; readonly payload: Record; readonly polls?: number; readonly elapsedMs?: number; readonly create?: Record; readonly probe?: Record; readonly diagnostics?: Record; readonly valuesRedacted: true; } export interface CompactCommandResult { readonly exitCode: number | null; readonly timedOut: boolean; readonly stdoutBytes: number; readonly stderrBytes: number; readonly stdoutPreview: string; readonly stderrPreview: string; } export interface ChildCliResult { readonly ok: boolean; readonly parsed: Record | null; readonly result: CompactCommandResult & { stdoutTail: string; stderrTail: string }; } export function sentinelSourceSnapshotStageRefPrefix(cicd: Record): string { const branch = stringAt(cicd, "source.branch"); const repository = stringAt(cicd, "source.repository"); const prefix = stringAt(cicd, "sourceSnapshot.stageRefPrefix") .replaceAll("{branch}", branch) .replaceAll("{repository}", repository) .replace(/\/+$/u, ""); if (!prefix.startsWith("refs/")) throw new Error("sourceSnapshot.stageRefPrefix must resolve to a git ref prefix"); return prefix; } export function sentinelSourceSnapshotRef(cicd: Record, commit: string): string { return `${sentinelSourceSnapshotStageRefPrefix(cicd)}/${commit}`; } export function monitorWebCicdPlan(spec: HwlabRuntimeLaneSpec, cicd: Record): Record { return { stack: stringAtNullable(cicd, "monitorWeb.frontendStack") ?? "vue3-vendored-browser-build", runtimeMode: stringAtNullable(cicd, "monitorWeb.runtimeMode") ?? "runner-served-bridge", assetRoot: stringAtNullable(cicd, "monitorWeb.assetRoot") ?? "scripts/assets/web-probe-sentinel-monitor-web", verifyCommand: "bun scripts/verify-web-probe-sentinel-monitor-web.ts", gitMirrorReadUrl: stringAt(cicd, "source.gitMirrorReadUrl"), sourceMode: stringAt(cicd, "builder.sourceMode"), envReuseMode: stringAtNullable(cicd, "monitorWeb.envReuse.mode") ?? "k8s-buildkit-and-ci-node-deps", envReuseNodeDepsPath: stringAtNullable(cicd, "monitorWeb.envReuse.nodeDepsPath") ?? "/opt/hwlab-ci-node-deps/node_modules", verifyPhase: stringAtNullable(cicd, "monitorWeb.imageBuild.verifyPhase") ?? "pre-image-build", imageBuildBuilder: spec.buildkit?.sidecarImage ?? null, imageBuildPackageMode: stringAtNullable(cicd, "monitorWeb.imageBuild.packageMode") ?? "copy-only-containerfile", imageBuildNetworkMode: monitorWebImageBuildNetworkMode(cicd), imageBuildProxySource: stringAtNullable(cicd, "monitorWeb.imageBuild.proxySource") ?? "node.networkProfile.imageBuildProxy", imageBuildContextIgnore: stringAtNullable(cicd, "monitorWeb.imageBuild.contextIgnore") ?? "generated", imageBuildState: monitorWebBuildkitStatePlan(cicd), ciBudgetSeconds: numberAtNullable(cicd, "monitorWeb.ciBudget.maxSeconds") ?? numberAt(cicd, "confirmWait.maxSeconds"), valuesRedacted: true, }; } export function monitorWebImageBuildNetworkMode(cicd: Record): "default" | "host" { const value = stringAtNullable(cicd, "monitorWeb.imageBuild.networkMode") ?? "host"; if (value !== "default" && value !== "host") throw new Error(`monitorWeb.imageBuild.networkMode must be default or host, got ${value}`); return value; } export function monitorWebBuildkitStatePlan(cicd: Record): Record { const state = recordTarget(valueAtPath(cicd, "monitorWeb.imageBuild.buildkitState"), "monitorWeb.imageBuild.buildkitState"); const mode = stringAt(state, "mode"); if (mode === "hostPath") { return { mode, path: stringAt(state, "path"), type: stringAt(state, "type"), valuesRedacted: true, }; } if (mode === "persistentVolumeClaim") { return { mode, claimName: stringAt(state, "claimName"), valuesRedacted: true, }; } if (mode === "emptyDir") { return { mode, sizeLimit: stringAt(state, "sizeLimit"), valuesRedacted: true, }; } throw new Error(`monitorWeb.imageBuild.buildkitState.mode must be hostPath, persistentVolumeClaim or emptyDir, got ${mode}`); } export function secretSourcePaths(sourceRef: string): string[] { if (sourceRef.startsWith(".env/")) return ownerFileSourcePaths(sourceRef); const paths = [join(repoRoot, ".state", "secrets", sourceRef)]; const marker = "/.worktree/"; const index = repoRoot.indexOf(marker); if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", sourceRef)); return [...new Set(paths)]; } export function ownerFileSourcePaths(sourceRef: string): string[] { if (sourceRef.includes("..") || sourceRef.includes("\0")) return []; const marker = "/.worktree/"; const index = repoRoot.indexOf(marker); const roots = index >= 0 ? [repoRoot.slice(0, index), repoRoot] : [repoRoot]; return [...new Set(roots.map((root) => join(root, sourceRef)))]; } export function parseEnvFile(textValue: string): Record { const values: Record = {}; for (const rawLine of textValue.split(/\r?\n/u)) { const line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) continue; const index = line.indexOf("="); if (index <= 0) continue; const key = line.slice(0, index).trim(); let value = line.slice(index + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1); values[key] = value; } return values; } export function stringAtNullable(value: unknown, path: string): string | null { const found = valueAtPath(value, path); return typeof found === "string" && found.length > 0 ? found : null; } export function numberAtNullable(value: unknown, path: string): number | null { const found = valueAtPath(value, path); return typeof found === "number" && Number.isFinite(found) ? found : null; } export function booleanAtNullable(value: unknown, path: string): boolean | null { const found = valueAtPath(value, path); return typeof found === "boolean" ? found : null; } export function displayPath(pathValue: string): string { if (pathValue.startsWith(`${repoRoot}/`)) return pathValue.slice(repoRoot.length + 1); const marker = "/.worktree/"; const index = repoRoot.indexOf(marker); if (index >= 0) { const mainRoot = repoRoot.slice(0, index); if (pathValue.startsWith(`${mainRoot}/`)) return pathValue.slice(mainRoot.length + 1); } return pathValue; } export function sentinelPipelineRunName(state: SentinelCicdState, rerun = false): string { const commit = state.sourceHead.commit ?? "source"; const base = `hwlab-web-probe-sentinel-${safeKubernetesSegment(state.sentinelId, 24)}-${commit.slice(0, 12)}`; if (!rerun) return base; const suffix = `-r${Date.now().toString(36)}`; return `${base.slice(0, Math.max(1, 63 - suffix.length)).replace(/-+$/u, "")}${suffix}`; } export function sentinelCliSuffix(state: SentinelCicdState): string { return ` --sentinel ${state.sentinelId}`; } export function safeJobSegment(value: string): string { return value.replace(/[^A-Za-z0-9_]+/gu, "_").replace(/^_+|_+$/gu, "").slice(0, 48) || "sentinel"; } export function safeKubernetesSegment(value: string, maxLength: number): string { const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, ""); return (normalized || "sentinel").slice(0, Math.max(1, maxLength)).replace(/-+$/u, "") || "sentinel"; } export function renderPublishResult(publish: Record): string { const payload = record(publish.payload); const diagnostics = record(publish.diagnostics); const diagnosticEnvReuse = record(diagnostics.envReuse); const envReuse = Object.keys(record(payload.envReuse)).length > 0 ? record(payload.envReuse) : diagnosticEnvReuse; const imageBuild = record(payload.imageBuild); const imageBuildProxy = record(imageBuild.proxy); const payloadStageTimings = record(payload.stageTimings); const diagnosticStageTimings = record(diagnostics.stageTimings); const timings = Object.keys(payloadStageTimings).length > 0 ? payloadStageTimings : diagnosticStageTimings; const commands = record(diagnostics.commands); const proxySummary = [imageBuildProxy.httpProxyPresent, imageBuildProxy.httpsProxyPresent, imageBuildProxy.allProxyPresent].some((item) => item === true) ? "present" : "none"; const runColumn = diagnostics.resourceKind === "PipelineRun" || publish.resourceKind === "PipelineRun" ? "PIPELINERUN" : "JOB"; const lines = [ "PUBLISH", table(["OK", "PHASE", runColumn, "ELAPSED", "POD", "CURRENT", "DIGEST", "GITOPS"], [[ publish.ok, publish.phase, publish.jobName, publish.elapsedMs ?? "-", diagnostics.pod ?? "-", diagnostics.currentPhase ?? "-", short(payload.digestRef), short(payload.gitopsCommit), ]]), ]; if (Object.keys(envReuse).length > 0) { lines.push( "", "PUBLISH_ENV_REUSE", table(["MODE", "NODE_DEPS", "PRESENT", "ENTRIES", "LINKED", "DEPENDENCY"], [[ envReuse.mode, envReuse.nodeDepsPath, envReuse.nodeDepsPresent, envReuse.nodeDepsEntries, envReuse.linkedNodeDeps ?? "-", envReuse.dependencyReuse, ]]), ); } if (Object.keys(imageBuild).length > 0 || Object.keys(timings).length > 0) { lines.push( "", "PUBLISH_BUILD", table(["BUILDER", "PACKAGE", "NETWORK", "PROXY", "IGNORE", "CACHE", "CACHE_LINES", "STEP_LINES", "SOURCE_MS", "VERIFY_MS", "IMAGE_MS", "GITOPS_MS", "TOTAL_MS"], [[ imageBuild.builder ?? "-", imageBuild.packageMode ?? "-", imageBuild.networkMode ?? "-", proxySummary, imageBuild.contextIgnoreEntries ?? "-", imageBuild.layerCache ?? "-", imageBuild.cacheHitLines ?? "-", imageBuild.stepLines ?? "-", timings.sourceFetchMs ?? "-", timings.monitorWebVerifyMs ?? "-", timings.imageBuildMs ?? "-", timings.gitopsMs ?? "-", timings.totalMs ?? payload.elapsedMs ?? "-", ]]), ); } if (Object.keys(diagnostics).length > 0) { lines.push( "", "PUBLISH_DIAGNOSTICS", table(["TASKRUN", "POD_PHASE", "ACTIVE", "CONDITION", "COMPLETED", "RECENT_LOG"], [[ diagnostics.taskRun ?? "-", diagnostics.podPhase ?? "-", diagnostics.active ?? "-", diagnostics.conditionReason ?? diagnostics.conditionStatus ?? "-", Array.isArray(diagnostics.completedStages) ? diagnostics.completedStages.join(",") : "-", diagnostics.recentLogSummary ?? "-", ]]), ); } if (publish.ok !== true && Object.keys(commands).length > 0) { lines.push( "", "PUBLISH_DRILLDOWN", ` status: ${commands.cliStatus ?? "-"}`, ` logs: ${commands.logs ?? "-"}`, ` describe: ${commands.describe ?? "-"}`, ` publish-current: ${commands.publishCurrent ?? "-"}`, ` git-mirror: ${commands.gitMirrorStatus ?? "-"}`, ` sync: ${commands.gitMirrorSync ?? "-"}`, ` flush: ${commands.gitMirrorFlush ?? "-"}`, ` apply: ${commands.controlPlaneApply ?? "-"}`, ); } return lines.join("\n"); } export function renderPublishCurrentResult(result: Record): string { const source = record(result.source); const image = record(result.image); const controlPlane = record(result.controlPlane); const publish = record(controlPlane.publish); const publishPayload = record(publish.payload); const observed = record(controlPlane.observed); const gitops = record(observed.gitops); const argo = record(observed.argo); const runtime = record(observed.runtime); const runtimeDeployment = record(record(runtime.probe).deployment); const health = record(result.health); const healthBody = record(record(health.health).bodyJson); const timings = record(result.timings); const budget = record(result.budget); const stageBudgets = record(result.stageBudgets); const validationPlan = record(result.validationPlan); const blocker = record(result.blocker); const recoveryNext = record(controlPlane.recoveryNext); const next = record(result.next); const warnings = Array.isArray(result.warnings) ? result.warnings : []; const slowStages = Array.isArray(result.slowStages) ? result.slowStages.map(record) : []; const lines = [ String(result.command), "", table(["NODE", "LANE", "SENTINEL", "STATUS", "MODE", "BUDGET_S", "ELAPSED_S"], [[ result.node, result.lane, result.sentinelId, result.ok === true ? "ok" : "blocked", result.mode, budget.maxSeconds ?? "-", finiteNumberOrNull(result.elapsedMs) === null ? "-" : Math.round((finiteNumberOrNull(result.elapsedMs) ?? 0) / 1000), ]]), "", table(["SOURCE", "COMMIT", "AUTHORITY", "STAGE_REF", "IMAGE_REF", "DIGEST", "PIPELINERUN"], [[ `${source.repository ?? "-"}@${source.branch ?? "-"}`, short(source.commit), source.sourceAuthority ?? "-", short(source.stageRef), image.ref ?? "-", short(publishPayload.digestRef ?? record(record(observed.registry).probe).digest), result.pipelineRun ?? publish.jobName ?? "-", ]]), "", table(["GITOPS_REV", "ARGO_REV", "ARGO", "RUNTIME_IMAGE", "RUNTIME_READY", "HEALTH"], [[ short(gitops.revision), short(argo.revision), `${argo.syncStatus ?? "-"}/${argo.healthStatus ?? "-"}`, short(runtimeDeployment.image), `${runtimeDeployment.readyReplicas ?? "-"}/${runtimeDeployment.desiredReplicas ?? "-"}`, health.ok === true ? "pass" : health.skipped === true ? `skipped:${text(health.reason)}` : Object.keys(health).length === 0 ? "planned" : "blocked", ]]), "", table(["SOURCE_SYNC_MS", "SOURCE_FETCH_MS", "VERIFY_MS", "IMAGE_MS", "GITOPS_MS", "ARGO_RUNTIME_MS", "VALIDATION_MS", "TOTAL_MS"], [[ timings.sourceSyncMs ?? "-", timings.sourceFetchMs ?? "-", timings.monitorWebVerifyMs ?? "-", timings.imageBuildMs ?? "-", timings.gitopsMs ?? "-", timings.argoRuntimeMs ?? "-", timings.healthValidationMs ?? "-", timings.totalMs ?? "-", ]]), "", table(["BUDGET_SOURCE", "SOURCE_SYNC", "SOURCE_FETCH", "VERIFY", "IMAGE", "GITOPS", "ARGO_RUNTIME", "VALIDATION"], [[ "YAML publishCurrent", stageBudgets.sourceSyncSeconds ?? "-", stageBudgets.sourceFetchSeconds ?? "-", stageBudgets.monitorWebVerifySeconds ?? "-", stageBudgets.imageBuildSeconds ?? "-", stageBudgets.gitopsSeconds ?? "-", stageBudgets.argoRuntimeSeconds ?? "-", stageBudgets.dashboardVerifySeconds ?? "-", ]]), ]; if (Object.keys(publish).length > 0) { const payloadImageBuild = record(publishPayload.imageBuild); const payloadEnvReuse = record(publishPayload.envReuse); lines.push( "", table(["ENV_REUSE", "NODE_DEPS", "BUILD_PACKAGE", "BUILD_NETWORK", "CACHE", "CACHE_LINES"], [[ payloadEnvReuse.dependencyReuse ?? "-", payloadEnvReuse.nodeDepsPresent ?? "-", payloadImageBuild.packageMode ?? "-", payloadImageBuild.networkMode ?? "-", payloadImageBuild.layerCache ?? "-", payloadImageBuild.cacheHitLines ?? "-", ]]), ); } lines.push( "", Object.keys(health).length === 0 ? "HEALTH_VALIDATION\n-" : table(["ENDPOINT", "HTTP", "OK", "STATUS", "PUBLIC_URL", "INTERNAL_URL"], [[ health.endpoint ?? validationPlan.endpoint ?? "-", health.httpStatus ?? "-", healthBody.ok ?? "-", healthBody.status ?? "-", health.publicUrl ?? "-", health.internalUrl ?? "-", ]]), "", slowStages.length === 0 ? "SLOW_STAGES\n-" : [ "SLOW_STAGES", table(["STAGE", "ELAPSED_MS", "BUDGET_S", "SUGGESTION"], slowStages.map((stage) => [stage.stage, stage.elapsedMs, stage.budgetSeconds, stage.suggestion])), ].join("\n"), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((item) => `- ${text(item)}`)].join("\n"), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : ["BLOCKER", table(["CODE", "REASON"], [[blocker.code, blocker.reason]])].join("\n"), "", Object.keys(recoveryNext).length === 0 ? "RECOVERY_NEXT\n-" : [ "RECOVERY_NEXT", table(["REASON", "PIPELINERUN", "DIGEST", "GITOPS"], [[recoveryNext.reason, recoveryNext.pipelineRun ?? "-", short(recoveryNext.digestRef), short(recoveryNext.gitopsCommit)]]), ` publish-current: ${recoveryNext.publishCurrent ?? "-"}`, ` status: ${recoveryNext.nextStatus ?? "-"}`, ` git-mirror: ${recoveryNext.gitMirrorStatus ?? "-"}`, ` sync: ${recoveryNext.gitMirrorSync ?? "-"}`, ` flush: ${recoveryNext.gitMirrorFlush ?? "-"}`, ` apply: ${recoveryNext.controlPlaneApply ?? "-"}`, ].join("\n"), "", "NEXT", ` publish-current: ${next.publishCurrent ?? "-"}`, ` status: ${next.controlPlaneStatus ?? "-"}`, ` post-deploy-dashboard: ${next.dashboardVerify ?? "-"}`, ` git-mirror: ${next.gitMirrorStatus ?? "-"}`, ` sync: ${next.gitMirrorSync ?? "-"}`, ` flush: ${next.gitMirrorFlush ?? "-"}`, "", "DISCLOSURE", ` end-to-end and stage budgets are read from ${Object.keys(validationPlan).length > 0 ? "publishCurrent YAML and runtime.healthPath" : "YAML-required publishCurrent fields"}.`, " CI/CD validation only checks the configured health endpoint; web-probe, Playwright and browser dashboard checks are post-deploy evidence, not this gate.", " image build uses Tekton PipelineRun and BuildKit; this command does not require Docker daemon/socket/build.", ); return lines.join("\n"); } export function renderImageResult(result: Record): string { const source = record(result.source); const sourceMirror = record(result.sourceMirror); const sourceMirrorSync = record(result.sourceMirrorSync); const image = record(result.image); const monitorWeb = record(image.monitorWeb); const registry = record(result.registry); const publish = record(result.publish); const blocker = record(result.blocker); const next = record(result.next); const warnings = Array.isArray(result.warnings) ? result.warnings : []; return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "MODE", "MUTATION"], [[result.node, result.lane, result.ok === true ? "ok" : "blocked", result.mode, result.mutation]]), "", table(["SOURCE_REPO", "BRANCH", "COMMIT", "AUTHORITY", "STAGE_REF", "MIRROR"], [[source.repository, source.branch, short(source.commit), source.sourceAuthority ?? "-", short(source.stageRef), short(source.mirrorCommit)]]), "", Object.keys(sourceMirror).length === 0 ? "SOURCE_MIRROR\n-" : table(["OK", "MODE", "COMMIT", "EXPECTED", "READ_URL"], [[sourceMirror.ok, record(sourceMirror.probe).mode, short(record(sourceMirror.probe).commit), short(record(sourceMirror.probe).expectedCommit), record(sourceMirror.probe).readUrl ?? "-"]]), "", table(["IMAGE", "BASE", "ENTRYPOINT", "DOCKERFILE"], [[image.ref, image.baseImage, image.entrypoint, short(image.dockerfileSha256)]]), "", Object.keys(monitorWeb).length === 0 ? "MONITOR_WEB\n-" : table(["STACK", "MODE", "ASSETS", "VERIFY", "ENV_REUSE", "IMAGE_BUILDER", "BUILD_PKG", "BUILD_NET", "BUILD_STATE", "CTX_IGNORE"], [[monitorWeb.stack, monitorWeb.runtimeMode, monitorWeb.assetRoot, monitorWeb.verifyCommand, `${monitorWeb.envReuseMode}:${monitorWeb.envReuseNodeDepsPath}`, monitorWeb.imageBuildBuilder ?? "-", monitorWeb.imageBuildPackageMode, monitorWeb.imageBuildNetworkMode, `${record(monitorWeb.imageBuildState).mode ?? "-"}:${record(monitorWeb.imageBuildState).path ?? record(monitorWeb.imageBuildState).claimName ?? record(monitorWeb.imageBuildState).sizeLimit ?? "-"}`, monitorWeb.imageBuildContextIgnore]]), "", Object.keys(registry).length === 0 ? "REGISTRY\n-" : table(["PROBED", "PRESENT", "DIGEST"], [[record(registry.probe).url ?? "-", record(registry.probe).present ?? "-", short(record(registry.probe).digest)]]), "", Object.keys(sourceMirrorSync).length === 0 ? "SOURCE_MIRROR_SYNC\n-" : table(["OK", "PHASE", "JOB", "COMMIT", "STAGE_REF", "ELAPSED"], [[sourceMirrorSync.ok, sourceMirrorSync.phase, sourceMirrorSync.jobName, short(record(sourceMirrorSync.payload).mirrorCommit), short(record(sourceMirrorSync.payload).stageRef), sourceMirrorSync.elapsedMs ?? "-"]]), "", Object.keys(publish).length === 0 ? "PUBLISH\n-" : renderPublishResult(publish), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((item) => `- ${text(item)}`)].join("\n"), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : ["BLOCKER", table(["CODE", "REASON"], [[blocker.code, blocker.reason]])].join("\n"), "", "NEXT", ` status: ${next.status ?? "-"}`, ` dry-run: ${next.dryRun ?? "-"}`, ` confirm: ${next.confirm ?? "-"}`, ` trigger: ${next.controlPlaneTrigger ?? "-"}`, ` control-plane: ${next.controlPlanePlan ?? "-"}`, "", "DISCLOSURE", " valuesRedacted=true; image status shows refs, hashes and object names only.", ].join("\n"); } export function renderControlPlaneResult(result: Record): string { const source = record(result.source); const image = record(result.image); const gitops = record(result.gitops); const argo = record(result.argo); const validation = record(result.validation); const observability = record(result.observability); const observed = record(result.observed); const sourceMirrorSync = record(result.sourceMirrorSync); const publish = record(result.publish); const flush = record(result.flush); const runtimeSecretsApply = record(result.runtimeSecretsApply); const publicExposureApply = record(result.publicExposureApply); const publicExposureCaddy = record(publicExposureApply.caddy); const argoApply = record(result.argoApply); const blocker = record(result.blocker); const statusDiagnosis = record(result.statusDiagnosis); const targetValidation = record(result.targetValidation); const targetValidationBusiness = record(targetValidation.businessStatus); const recoveryNext = record(result.recoveryNext); const next = record(result.next); const warnings = Array.isArray(result.warnings) ? result.warnings : []; return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "MODE", "PIPELINERUN"], [[result.node, result.lane, result.ok === true ? "ok" : "blocked", result.mode, result.pipelineRun]]), "", table(["SOURCE", "COMMIT", "AUTHORITY", "STAGE_REF", "IMAGE", "MANIFEST"], [[`${source.repository}@${source.branch}`, short(source.commit), source.sourceAuthority ?? "-", short(source.stageRef), image.ref, short(gitops.manifestSha256)]]), "", table(["GITOPS_PATH", "ARGO_APP", "TARGET_REV", "OBJECTS"], [[gitops.path, argo.applicationName, gitops.targetRevision, gitops.manifestObjects]]), "", table(["SCENARIO", "MAX_SECONDS", "CI_WAIT", "QVERIFY", "SECOND_PATH"], [[validation.scenarioId, validation.maxSeconds, validation.controlPlaneWaitMaxSeconds ?? "-", validation.quickVerifyMode ?? "-", validation.automaticSecondPath]]), "", Object.keys(observability).length === 0 ? "OTEL\n-" : table(["ENABLED", "ENDPOINT", "SERVICE", "COVERAGE"], [[observability.enabled, observability.endpointConfigured, observability.serviceName, observability.coverage]]), "", renderObservedStatus(observed), "", Object.keys(statusDiagnosis).length === 0 ? "STATUS_DIAGNOSIS\n-" : [ "STATUS_DIAGNOSIS", table(["CODE", "PHASE", "PIPELINERUN", "SOURCE", "REGISTRY", "GIT_MIRROR", "GITOPS", "ARGO", "RUNTIME"], [[ statusDiagnosis.code, statusDiagnosis.phase, statusDiagnosis.pipelineRun, statusDiagnosis.sourceMirror, statusDiagnosis.registry, statusDiagnosis.gitMirror, statusDiagnosis.gitops, statusDiagnosis.argo, statusDiagnosis.runtime, ]]), ].join("\n"), "", Object.keys(sourceMirrorSync).length === 0 ? "SOURCE_MIRROR_SYNC\n-" : table(["OK", "PHASE", "JOB", "COMMIT", "STAGE_REF", "ELAPSED"], [[sourceMirrorSync.ok, sourceMirrorSync.phase, sourceMirrorSync.jobName, short(record(sourceMirrorSync.payload).mirrorCommit), short(record(sourceMirrorSync.payload).stageRef), sourceMirrorSync.elapsedMs ?? "-"]]), "", Object.keys(targetValidation).length === 0 ? "TARGET_VALIDATION\n-" : table(["OK", "STATUS", "BUSINESS", "ERROR_TITLE", "SCENARIO", "RUN", "OBSERVER", "REPORT", "FINDINGS", "ARTIFACTS"], [[ targetValidation.ok, targetValidation.status, targetValidationBusiness.status ?? "-", targetValidation.errorTitleZh ?? targetValidation.failureTitleZh ?? targetValidationBusiness.errorTitleZh ?? "-", targetValidation.scenarioId, targetValidation.runId, targetValidation.observerId, short(targetValidation.reportJsonSha256), targetValidation.findingCount, targetValidation.artifactCount, ]]), "", Object.keys(publish).length === 0 ? "PUBLISH\n-" : renderPublishResult(publish), "", Object.keys(flush).length === 0 ? "FLUSH\n-" : flush.mode === "async-job" ? table(["OK", "MODE", "JOB", "STATUS"], [[flush.ok, flush.mode, record(flush.job).id, record(flush.next).status]]) : table(["OK", "EXIT", "TIMED_OUT", "PREVIEW"], [[flush.ok, record(flush.result).exitCode, record(flush.result).timedOut, record(flush.result).stdoutPreview]]), "", Object.keys(runtimeSecretsApply).length === 0 ? "RUNTIME_SECRETS\n-" : table(["OK", "SECRETS", "KEYS", "SKIPPED"], [[runtimeSecretsApply.ok, runtimeSecretsApply.secretCount ?? "-", runtimeSecretsApply.keyCount ?? "-", runtimeSecretsApply.skippedKeyCount ?? "-"]]), "", Object.keys(publicExposureApply).length === 0 ? "PUBLIC_EXPOSURE_APPLY\n-" : table(["OK", "SECRET", "CADDY", "HOST", "ROUTE_HTTP"], [[publicExposureApply.ok, record(publicExposureApply.secret).ok, record(publicExposureApply.caddy).ok, publicExposureApply.hostname, record(publicExposureApply.caddy).routeProbeHttpStatus ?? "-"]]), "", Object.keys(publicExposureCaddy).length === 0 || publicExposureCaddy.ok === true ? "CADDY_APPLY_DETAIL\n-" : table(["PY", "VALIDATE", "RELOAD", "PROBE", "HTTP", "BLOCK", "ACTIVE", "ERROR", "STDOUT", "STDERR"], [[publicExposureCaddy.pythonExitCode, publicExposureCaddy.validateExitCode, publicExposureCaddy.reloadExitCode, publicExposureCaddy.routeProbeExitCode, publicExposureCaddy.routeProbeHttpStatus, publicExposureCaddy.afterBlockPresent, publicExposureCaddy.active, short(publicExposureCaddy.errorPreview), short(record(publicExposureCaddy.result).stdoutPreview), short(record(publicExposureCaddy.result).stderrPreview)]]), "", Object.keys(argoApply).length === 0 ? "ARGO_APPLY\n-" : table(["OK", "EXIT", "PREVIEW"], [[argoApply.ok, record(argoApply.result).exitCode, record(argoApply.result).stdoutPreview]]), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((item) => `- ${text(item)}`)].join("\n"), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : ["BLOCKER", table(["CODE", "REASON"], [[blocker.code, blocker.reason]])].join("\n"), "", Object.keys(recoveryNext).length === 0 ? "RECOVERY_NEXT\n-" : [ "RECOVERY_NEXT", table(["REASON", "PIPELINERUN", "DIGEST", "GITOPS"], [[recoveryNext.reason, recoveryNext.pipelineRun ?? "-", short(recoveryNext.digestRef), short(recoveryNext.gitopsCommit)]]), ` publish-current: ${recoveryNext.publishCurrent ?? "-"}`, ` status: ${recoveryNext.nextStatus ?? "-"}`, ` git-mirror: ${recoveryNext.gitMirrorStatus ?? "-"}`, ` sync: ${recoveryNext.gitMirrorSync ?? "-"}`, ` flush: ${recoveryNext.gitMirrorFlush ?? "-"}`, ` apply: ${recoveryNext.controlPlaneApply ?? "-"}`, ].join("\n"), "", "NEXT", ` plan: ${next.plan ?? "-"}`, ` status: ${next.status ?? "-"}`, ` image: ${next.image ?? "-"}`, ` trigger-current: ${next.triggerCurrent ?? "-"}`, ` apply: ${next.apply ?? "-"}`, ` validate: ${next.validate ?? "-"}`, ` quick-verify: ${next.quickVerify ?? "-"}`, ` git-mirror: ${next.gitMirrorStatus ?? "-"}`, ` sync: ${next.gitMirrorSync ?? "-"}`, ` flush: ${next.gitMirrorFlush ?? "-"}`, "", "DISCLOSURE", " default view is a bounded CI/CD summary; full manifest content is represented by object counts and sha256.", " sentinel unavailable policy is structured-failure; no automatic second execution path is rendered.", ].join("\n"); } export function renderObservedStatus(observed: Record): string { const rows = [ observedStatusRow("source", observed.sourceMirror), observedStatusRow("registry", observed.registry), observedStatusRow("git-mirror", observed.gitMirror), observedStatusRow("gitops", observed.gitops), observedStatusRow("argo", observed.argo), observedStatusRow("runtime", observed.runtime), observedStatusRow("cadence", observed.cadence), ].filter((row) => row !== null); if (rows.length === 0) return "OBSERVED\n-"; return table(["CHECK", "OK", "DETAIL", "EXIT", "TIMED_OUT", "PREVIEW"], rows); } export function observedStatusRow(name: string, value: unknown): unknown[] | null { const item = record(value); if (Object.keys(item).length === 0) return null; const result = record(item.result); return [name, item.ok, observedDetail(name, item), result.exitCode, result.timedOut, result.stdoutPreview]; } export function observedDetail(name: string, item: Record): string { if (name === "source") return `${record(item.probe).mode ?? "mirror"} ${short(record(item.probe).commit)}/${short(record(item.probe).expectedCommit)}`; if (name === "registry") return `${record(item.probe).present === true ? "present" : "missing"} ${short(record(item.probe).digest)}`; if (name === "git-mirror" && item.skipped === true) return `${item.reason ?? "skipped"}`; if (name === "gitops") return `${short(item.revision)} image=${short(item.image)}`; if (name === "argo") { const diagnostics = record(item.diagnostics); const problems = Array.isArray(diagnostics.problemResources) ? diagnostics.problemResources : []; const first = problems.find((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)) as Record | undefined; const problemText = Number(diagnostics.problemResourceCount ?? 0) > 0 ? ` degraded=${diagnostics.problemResourceCount}:${first?.kind ?? "-"} ${first?.namespace ?? "-"}/${first?.name ?? "-"} ${first?.healthStatus ?? first?.status ?? "-"}` : ""; return `${item.syncStatus ?? "-"} ${item.healthStatus ?? "-"} ${short(item.revision)}/${short(item.expectedRevision)}${problemText}`; } if (name === "runtime") { const probe = record(item.probe); const deployment = record(probe.deployment); return `ready=${deployment.readyReplicas ?? "-"} image=${short(deployment.image)}/${short(deployment.expectedImage)}`; } if (name === "cadence") { if (item.skipped === true) return `${item.reason ?? "skipped"}`; const probe = record(item.probe); return `${probe.code ?? "ok"} schedule=${probe.schedule ?? "-"}/${probe.expectedSchedule ?? "-"} last=${probe.lastScheduleTime ?? "-"} jobs=${probe.jobCount ?? "-"}`; } return "-"; } export function renderAsyncJobResult(result: Record): string { const job = record(result.job); const next = record(result.next); return [ String(result.command), "", table(["NODE", "LANE", "MODE", "MUTATION", "JOB"], [[result.node, result.lane, result.mode, result.mutation, job.id]]), "", table(["STATUS", "NAME", "CREATED"], [[job.status, job.name, job.createdAt]]), "", "NEXT", ` status: ${next.status ?? "-"}`, ` wait: ${next.wait ?? "-"}`, "", "DISCLOSURE", " confirmed operation is delegated to UniDesk job status to keep interactive calls bounded.", ].join("\n"); } export function rendered(ok: boolean, command: string, text: string): RenderedCliResult { return { ok, command, renderedText: `${text.trimEnd()}\n`, contentType: "text/plain" }; } export function sentinelProgressEvent(event: string, payload: Record): void { console.error(JSON.stringify({ event, at: new Date().toISOString(), ...payload, valuesRedacted: true })); } export function readConfigFile(file: string): unknown { if (file.startsWith("/") || file.includes("..") || !file.startsWith("config/")) throw new Error(`unsafe configRef file: ${file}`); const abs = rootPath(file); if (!existsSync(abs)) throw new Error(`${file} does not exist`); return Bun.YAML.parse(readFileSync(abs, "utf8")) as unknown; } export function configRefFile(ref: string): string { const [file, path, extra] = ref.split("#"); if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) throw new Error(`invalid configRef: ${ref}`); return file; } export function valueAtPath(value: unknown, path: string): unknown { let current: unknown = value; for (const segment of path.split(".")) { const match = /^([A-Za-z0-9_-]+)?(?:\[(\d+)\])?$/u.exec(segment); if (match === null) return undefined; if (match[1] !== undefined) { if (!isRecord(current)) return undefined; current = current[match[1]]; } if (match[2] !== undefined) { if (!Array.isArray(current)) return undefined; current = current[Number(match[2])]; } } return current; } export function stringAt(value: unknown, path: string): string { const found = valueAtPath(value, path); if (typeof found !== "string" || found.length === 0) throw new Error(`${path} must be a non-empty string`); return found; } export function nonEmptyString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } export function stringField(value: Record, path: string): string { return stringAt(value, path); } export function stringTarget(value: unknown, label: string): string { if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must resolve to a non-empty string`); return value; } export function numberAt(value: unknown, path: string): number { const found = valueAtPath(value, path); if (typeof found !== "number" || !Number.isFinite(found)) throw new Error(`${path} must be a number`); return found; } export function booleanAt(value: unknown, path: string): boolean { const found = valueAtPath(value, path); if (typeof found !== "boolean") throw new Error(`${path} must be a boolean`); return found; } export function finiteNumberOrNull(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } export function arrayAt(value: unknown, path: string): unknown[] { const found = valueAtPath(value, path); if (!Array.isArray(found)) throw new Error(`${path} must be an array`); return found; } export function arrayAtNullable(value: unknown, path: string): Record[] { const found = valueAtPath(value, path); return Array.isArray(found) ? found.map(record) : []; } export function recordTarget(value: unknown, label: string): Record { if (!isRecord(value)) throw new Error(`${label} must resolve to an object`); return value; } export function record(value: unknown): Record { return isRecord(value) ? value : {}; } export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function manifestObjectSummary(items: readonly Record[]): readonly Record[] { return items.map((item) => ({ kind: item.kind ?? null, name: record(item.metadata).name ?? null, namespace: record(item.metadata).namespace ?? null, })); } export function compactCommand(result: CommandResult): CompactCommandResult { return { exitCode: result.exitCode, timedOut: result.timedOut, stdoutBytes: Buffer.byteLength(result.stdout), stderrBytes: Buffer.byteLength(result.stderr), stdoutPreview: result.stdout.trim().slice(0, 500), stderrPreview: result.stderr.trim().slice(0, 500), }; } export function resolveSentinelChildJson(result: Pick, requestedStdoutType: string): { parsed: Record | null; diagnostics: Record } { const resolved = resolveCliChildJsonCommandResult({ result, requestedStdoutType, acceptParsed: (value) => Object.keys(value).length > 0, }); return { parsed: resolved.parsed, diagnostics: resolved.diagnostics }; } export function parseJsonObject(text: string): Record | null { const trimmed = text.trim(); if (trimmed.length === 0) return null; try { const parsed = JSON.parse(trimmed) as unknown; return isRecord(parsed) ? parsed : null; } catch { return null; } } export function table(headers: string[], rows: unknown[][]): string { const normalized = [headers, ...rows.map((row) => row.map(text))]; const widths = headers.map((_, index) => Math.max(...normalized.map((row) => text(row[index] ?? "").length))); return normalized.map((row) => row.map((cell, index) => text(cell).padEnd(widths[index])).join(" ").trimEnd()).join("\n"); } export function text(value: unknown): string { if (value === undefined || value === null || value === "") return "-"; if (typeof value === "boolean") return value ? "true" : "false"; return String(value).replace(/\s+/gu, " ").trim(); } export function short(value: unknown): string { const raw = text(value); if (raw === "-") return raw; if (/^sha256:[0-9a-f]{64}$/iu.test(raw)) return `${raw.slice(0, 19)}...`; if (/^[0-9a-f]{40}$/iu.test(raw)) return raw.slice(0, 12); return raw.length > 42 ? `${raw.slice(0, 39)}...` : raw; } export function sha256(textValue: string): string { return `sha256:${createHash("sha256").update(textValue).digest("hex")}`; } export function shellQuote(value: string): string { return `'${value.replace(/'/gu, "'\\''")}'`; } export function tomlEscape(value: string): string { return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"'); }