UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

723 lines (662 loc) • 27.4 kB
import type { JsonObject, JsonValue } from "./project-config.ts"; export const KXM_COMPILED_WORKFLOW_SCHEMA = "kxm.compiled-workflow.v1"; export const KXM_RESERVED_STEP_KINDS = ["workflow"] as const; const WORKFLOW_SCHEMA = "kxm.workflow.v1"; const STEP_ID_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/; const SUPPORTED_STEP_KINDS = new Set(["agent", "moa", "gate", "approval", "wait"]); const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]); const JOIN_STRATEGIES = new Set(["all", "all-settled", "quorum", "first-success"]); const EVIDENCE_KINDS = new Set(["assignment-result", "gate", "receipt", "approval", "artifact"]); const REPOSITORY_ACCESS = new Set(["none", "read", "write"]); const DISTINCT_BY = new Set(["provider", "model", "profile"]); const BANNED_RUNTIME_HINT = "not yet supported"; export type ReadonlyJsonValue = | string | number | boolean | null | readonly ReadonlyJsonValue[] | { readonly [key: string]: ReadonlyJsonValue }; export type ReadonlyJsonObject = { readonly [key: string]: ReadonlyJsonValue }; export type KxmTerminalStatus = "completed" | "failed" | "cancelled"; export type KxmCompiledStepKind = "agent" | "moa" | "gate" | "approval" | "wait"; export type KxmRepositoryAccess = "none" | "read" | "write"; export type KxmCompiledTransition = | { readonly to: "step"; readonly target: string; readonly edge: "forward" | "back"; readonly maxTransitions?: number } | { readonly to: "terminal"; readonly terminalStatus: KxmTerminalStatus; readonly maxTransitions?: number }; export interface KxmCompiledProducerPolicy { readonly minimumProducers: number; readonly eligibleAgents: readonly string[]; readonly acceptedStatuses: readonly ["passed"]; readonly degradation?: { readonly minimumProducers: number }; } export interface KxmCompiledEvidence { readonly key: string; readonly kind: "assignment-result" | "gate" | "receipt" | "approval" | "artifact"; readonly minimum: number; readonly reusableAcrossAttempts: boolean; readonly producerPolicy?: KxmCompiledProducerPolicy; } export interface KxmCompiledAssignments { readonly allowedAgents: readonly string[]; // declared, else [step.agent] when present, else [] readonly minimum: number; readonly target: number; readonly maximum: number; readonly maxParallel: number; readonly maxAttemptsPerAssignment: number; readonly maxWriteRepositories?: number; readonly distinctBy: readonly ("provider" | "model" | "profile")[]; } export interface KxmCompiledJoin { readonly strategy: "all" | "all-settled" | "quorum" | "first-success"; readonly minimumPassed?: number; readonly cancelRemaining?: boolean; } interface KxmCompiledStepBase { readonly id: string; readonly index: number; readonly description?: string; readonly instructions?: string; readonly maxAttempts: number; readonly timeoutMs?: number; readonly safeSpeculation: boolean; readonly repositories: Readonly<Record<string, KxmRepositoryAccess>>; readonly tools?: ReadonlyJsonObject; readonly secrets: readonly ReadonlyJsonObject[]; readonly model?: ReadonlyJsonValue; readonly requiredEvidence: readonly KxmCompiledEvidence[]; readonly outcomes: readonly string[]; readonly transitions: Readonly<Record<string, KxmCompiledTransition>>; readonly requiresPlanHash: boolean; readonly assignments: KxmCompiledAssignments; readonly join: KxmCompiledJoin; } export type KxmCompiledStep = | (KxmCompiledStepBase & { readonly kind: "agent" | "moa"; readonly agent: string }) | (KxmCompiledStepBase & { readonly kind: "gate"; readonly gate: string; readonly expect: "pass" | "fail" }) | (KxmCompiledStepBase & { readonly kind: "approval" }) | (KxmCompiledStepBase & { readonly kind: "wait"; readonly signal: string }); export interface KxmCompiledPlan { readonly schema: typeof KXM_COMPILED_WORKFLOW_SCHEMA; readonly workflowId: string; readonly sourcePath?: string; readonly coordinator: string; readonly limits: { readonly maxTransitions?: number; readonly maxRunDurationMs?: number; readonly maxAgentTimeMs?: number; readonly maxModelCost?: number; readonly currency?: string; }; readonly transitionBudget: number; readonly hasBackEdges: boolean; readonly entryStepId: string; readonly order: readonly string[]; readonly steps: Readonly<Record<string, KxmCompiledStep>>; readonly reproOracle?: { readonly stageId: string; readonly evidenceKey: string }; readonly planHash?: { readonly stageId: string; readonly evidenceKey: string }; readonly requirePlanHash: readonly string[]; } export interface KxmEngineCompileIssue { readonly code: string; readonly workflowId: string; readonly stepId?: string; readonly outcome?: string; readonly message: string; } export class KxmEngineCompileError extends Error { readonly issues: readonly KxmEngineCompileIssue[]; constructor(issues: readonly KxmEngineCompileIssue[]) { const sorted = sortIssues(issues); super(sorted.map((issue) => `${issue.workflowId}: ${issue.code}: ${issue.message}`).join("\n")); this.name = "KxmEngineCompileError"; this.issues = sorted; } } export interface KxmWorkflowCompileInput { readonly id: string; readonly value: JsonObject; readonly logicalPath?: string; } interface IssueSink { workflowId: string; issues: KxmEngineCompileIssue[]; } export function compileKxmWorkflow(input: KxmWorkflowCompileInput): KxmCompiledPlan { const sink: IssueSink = { workflowId: input.id, issues: [] }; const value = input.value; if (value.schema !== WORKFLOW_SCHEMA) { pushIssue(sink, { code: "workflow_schema_unsupported", message: "schema is not kxm.workflow.v1" }); } const rawSteps = valuesOf(value, "steps").map(objectValue).filter((step): step is JsonObject => Boolean(step)); if (rawSteps.length === 0) { pushIssue(sink, { code: "steps_empty", message: "workflow has no steps" }); } const stepIndex = new Map<string, number>(); for (const [index, step] of rawSteps.entries()) { const id = stringValue(step.id); if (!id || !isStepId(id)) { pushIssue(sink, { code: "step_id_invalid", message: "step id is invalid", ...(id ? { stepId: id } : {}) }); continue; } if (stepIndex.has(id)) { pushIssue(sink, { code: "step_id_duplicate", stepId: id, message: `step ${id} is duplicated` }); continue; } stepIndex.set(id, index); } const limitsObject = objectValue(value.limits); const limits = compileLimits(limitsObject, sink); const requirePlanHash = names(value.requirePlanHash); const compiledSteps: KxmCompiledStep[] = []; let hasBackEdges = false; for (const [index, step] of rawSteps.entries()) { const compiled = compileStep(step, index, stepIndex, requirePlanHash, sink); if (!compiled) continue; compiledSteps.push(compiled); if (Object.values(compiled.transitions).some((transition) => transition.to === "step" && transition.edge === "back")) { hasBackEdges = true; } } if (hasBackEdges && typeof limitsObject?.maxTransitions !== "number") { pushIssue(sink, { code: "workflow_cycle_unbounded", message: "workflow with back-edges requires limits.maxTransitions" }); } const reproOracle = compileOracle("reproOracle", objectValue(value.reproOracle), compiledSteps, sink); const planHash = compileOracle("planHash", objectValue(value.planHash), compiledSteps, sink); for (const stageId of requirePlanHash) { if (!stepIndex.has(stageId)) { pushIssue(sink, { code: "plan_hash_stage_unknown", message: `requirePlanHash references unknown stage ${stageId}` }); } } if (sink.issues.length > 0) throw new KxmEngineCompileError(sink.issues); const order = compiledSteps.map((step) => step.id); const steps = Object.create(null) as Record<string, KxmCompiledStep>; for (const step of compiledSteps) steps[step.id] = step; const entryStepId = order[0]; if (!entryStepId) throw new KxmEngineCompileError(sink.issues); const coordinator = stringValue(value.coordinator) ?? "coordinator"; const transitionBudget = limits.maxTransitions ?? order.length; const plan: KxmCompiledPlan = { schema: KXM_COMPILED_WORKFLOW_SCHEMA, workflowId: input.id, ...(input.logicalPath !== undefined ? { sourcePath: input.logicalPath } : {}), coordinator, limits, transitionBudget, hasBackEdges, entryStepId, order, steps, ...(reproOracle ? { reproOracle } : {}), ...(planHash ? { planHash } : {}), requirePlanHash: [...requirePlanHash], }; return deepFreeze(plan); } function compileLimits( limits: JsonObject | undefined, sink: IssueSink, ): KxmCompiledPlan["limits"] { if (!limits) return {}; const compiled: { maxTransitions?: number; maxRunDurationMs?: number; maxAgentTimeMs?: number; maxModelCost?: number; currency?: string; } = {}; if (limits.maxTransitions !== undefined) { if (isCount(limits.maxTransitions)) compiled.maxTransitions = limits.maxTransitions; else pushIssue(sink, { code: "count_invalid", message: "limits.maxTransitions must be an integer >= 1" }); } if (limits.maxRunDurationMs !== undefined) { if (isDuration(limits.maxRunDurationMs)) compiled.maxRunDurationMs = limits.maxRunDurationMs; else pushIssue(sink, { code: "duration_invalid", message: "limits.maxRunDurationMs must be an integer >= 0" }); } if (limits.maxAgentTimeMs !== undefined) { if (isDuration(limits.maxAgentTimeMs)) compiled.maxAgentTimeMs = limits.maxAgentTimeMs; else pushIssue(sink, { code: "duration_invalid", message: "limits.maxAgentTimeMs must be an integer >= 0" }); } if (limits.maxModelCost !== undefined) { if (isCost(limits.maxModelCost)) compiled.maxModelCost = limits.maxModelCost; else pushIssue(sink, { code: "cost_invalid", message: "limits.maxModelCost must be a finite number > 0" }); } if (limits.currency !== undefined) { if (typeof limits.currency === "string") compiled.currency = limits.currency; } return compiled; } function compileStep( step: JsonObject, index: number, stepIndex: ReadonlyMap<string, number>, requirePlanHash: readonly string[], sink: IssueSink, ): KxmCompiledStep | undefined { const id = stringValue(step.id); if (!id || !isStepId(id) || stepIndex.get(id) !== index) return undefined; const kind = stringValue(step.kind); if (kind === "workflow") { pushIssue(sink, { code: "step_kind_reserved", stepId: id, message: `kind workflow is ${BANNED_RUNTIME_HINT}` }); return undefined; } if (!kind || !SUPPORTED_STEP_KINDS.has(kind)) { pushIssue(sink, { code: "step_kind_unknown", stepId: id, message: `kind ${String(kind)} is unknown` }); return undefined; } const agent = stringValue(step.agent); const gate = stringValue(step.gate); const signal = stringValue(step.signal); if ((kind === "agent" || kind === "moa") && !agent) { pushIssue(sink, { code: "step_agent_missing", stepId: id, message: `${id} is missing agent` }); } if (kind === "gate" && !gate) { pushIssue(sink, { code: "step_gate_missing", stepId: id, message: `${id} is missing gate` }); } if (kind === "wait" && !signal) { pushIssue(sink, { code: "step_signal_missing", stepId: id, message: `${id} is missing signal` }); } if (step.expect !== undefined && (kind !== "gate" || (step.expect !== "pass" && step.expect !== "fail"))) { pushIssue(sink, { code: "gate_expect_invalid", stepId: id, message: "expect is gate-only pass or fail" }); } if (kind === "gate" && Object.keys(objectValue(step.on) ?? {}).some((outcome) => outcome === "implementation_failure" || outcome === "repro_missing")) { pushIssue(sink, { code: "gate_outcome_renamed", stepId: id, message: "use implementation-failure and repro-missing" }); } const maxAttempts = compileCountField(step.maxAttempts, 1, `${id}.maxAttempts`, id, sink); const timeoutMs = compileOptionalDuration(step.timeoutMs, `${id}.timeoutMs`, id, sink); const assignments = compileAssignments(step, id, agent, sink); const join = compileJoin(step, id, sink); const requiredEvidence = compileEvidence(step, id, sink); const transitions = compileTransitions(step, id, index, stepIndex, sink); const outcomes = Object.keys(transitions).sort(compareCodeUnits); const orderedTransitions = Object.create(null) as Record<string, KxmCompiledTransition>; for (const outcome of outcomes) { const transition = transitions[outcome]; if (transition) orderedTransitions[outcome] = transition; } const description = stringValue(step.description); const instructions = stringValue(step.instructions); const base: KxmCompiledStepBase = { id, index, ...(description !== undefined ? { description } : {}), ...(instructions !== undefined ? { instructions } : {}), maxAttempts, ...(timeoutMs !== undefined ? { timeoutMs } : {}), safeSpeculation: step.safeSpeculation === true, repositories: compileRepositories(step.repositories), ...(isJsonObject(step.tools) ? { tools: cloneJsonObject(step.tools) } : {}), secrets: compileSecrets(step.secrets), ...(step.model !== undefined ? { model: cloneJsonValue(step.model) } : {}), requiredEvidence, outcomes, transitions: orderedTransitions, requiresPlanHash: requirePlanHash.includes(id), assignments, join, }; if (kind === "agent" || kind === "moa") { if (!agent) return undefined; return { ...base, kind, agent }; } if (kind === "gate") { if (!gate) return undefined; return { ...base, kind, gate, expect: step.expect === "fail" ? "fail" : "pass" }; } if (kind === "wait") { if (!signal) return undefined; return { ...base, kind, signal }; } return { ...base, kind: "approval" }; } function compileAssignments( step: JsonObject, stepId: string, primaryAgentId: string | undefined, sink: IssueSink, ): KxmCompiledAssignments { const assignment = objectValue(step.assignments); const declaredAgents = assignment ? names(assignment.allowedAgents) : []; const allowedAgents = declaredAgents.length > 0 ? declaredAgents : primaryAgentId ? [primaryAgentId] : []; // Mirrors validateWorkflow in project-config.ts (minimum 1, target = minimum, maximum = target, maxParallel = maximum) for every kind. Do not resolve kind-conditional defaults here; the loader is the ceiling authority. Phase 7 changes both together. const minimum = compileCountField(assignment?.minimum, 1, `${stepId}.assignments.minimum`, stepId, sink); const target = compileCountField(assignment?.target, minimum, `${stepId}.assignments.target`, stepId, sink); const maximum = compileCountField(assignment?.maximum, target, `${stepId}.assignments.maximum`, stepId, sink); const maxParallel = compileCountField(assignment?.maxParallel, maximum, `${stepId}.assignments.maxParallel`, stepId, sink); const maxAttemptsPerAssignment = compileCountField( assignment?.maxAttemptsPerAssignment, 1, `${stepId}.assignments.maxAttemptsPerAssignment`, stepId, sink, ); const maxWriteRepositories = compileOptionalCount( assignment?.maxWriteRepositories, `${stepId}.assignments.maxWriteRepositories`, stepId, sink, ); const distinctBy = names(assignment?.distinctBy).filter((value): value is "provider" | "model" | "profile" => DISTINCT_BY.has(value)); return { allowedAgents: [...allowedAgents], minimum, target, maximum, maxParallel, maxAttemptsPerAssignment, ...(maxWriteRepositories !== undefined ? { maxWriteRepositories } : {}), distinctBy, }; } function compileJoin(step: JsonObject, stepId: string, sink: IssueSink): KxmCompiledJoin { const join = objectValue(step.join); if (!join) return { strategy: "all" }; const declared = stringValue(join.strategy); const strategy = declared && JOIN_STRATEGIES.has(declared) ? declared as KxmCompiledJoin["strategy"] : "all"; const minimumPassed = compileOptionalCount(join.minimumPassed, `${stepId}.join.minimumPassed`, stepId, sink); const compiled: KxmCompiledJoin = { strategy, ...(minimumPassed !== undefined ? { minimumPassed } : {}), ...(typeof join.cancelRemaining === "boolean" ? { cancelRemaining: join.cancelRemaining } : {}), }; return compiled; } function compileEvidence(step: JsonObject, stepId: string, sink: IssueSink): KxmCompiledEvidence[] { const evidence: KxmCompiledEvidence[] = []; const seen = new Set<string>(); for (const candidate of valuesOf(step, "requiredEvidence")) { const requirement = objectValue(candidate); if (!requirement) continue; const key = stringValue(requirement.key); if (!key) continue; if (seen.has(key)) { pushIssue(sink, { code: "evidence_key_duplicate", stepId, message: `${stepId} evidence key ${key} is duplicated` }); continue; } seen.add(key); const kind = stringValue(requirement.kind); if (!kind || !EVIDENCE_KINDS.has(kind)) continue; const minimum = compileCountField(requirement.minimum, 1, `${stepId}.${key}.minimum`, stepId, sink); const producerPolicy = objectValue(requirement.producerPolicy); const compiled: KxmCompiledEvidence = { key, kind: kind as KxmCompiledEvidence["kind"], minimum, reusableAcrossAttempts: requirement.reusableAcrossAttempts === true, ...(producerPolicy ? { producerPolicy: compileProducerPolicy(producerPolicy, stepId, key, sink) } : {}), }; evidence.push(compiled); } return evidence; } function compileProducerPolicy( policy: JsonObject, stepId: string, key: string, sink: IssueSink, ): KxmCompiledProducerPolicy { const minimumProducers = compileCountField( policy.minimumProducers, 1, `${stepId}.${key}.producerPolicy.minimumProducers`, stepId, sink, ); const degradation = objectValue(policy.degradation); const degradationMinimum = compileOptionalCount( degradation?.minimumProducers, `${stepId}.${key}.producerPolicy.degradation.minimumProducers`, stepId, sink, ); return { minimumProducers, eligibleAgents: names(policy.eligibleAgents), acceptedStatuses: ["passed"], ...(degradationMinimum !== undefined ? { degradation: { minimumProducers: degradationMinimum } } : {}), }; } function compileTransitions( step: JsonObject, stepId: string, index: number, stepIndex: ReadonlyMap<string, number>, sink: IssueSink, ): Record<string, KxmCompiledTransition> { const outcomes = objectValue(step.on); const compiled = Object.create(null) as Record<string, KxmCompiledTransition>; if (!outcomes || Object.keys(outcomes).length === 0) { pushIssue(sink, { code: "transitions_missing", stepId, message: `${stepId} has no declared transitions` }); return compiled; } for (const [outcome, raw] of Object.entries(outcomes)) { const parsed = parseTransition(raw); const maxTransitions = compileOptionalCount( parsed.maxTransitions, `${stepId}.${outcome}.maxTransitions`, stepId, sink, outcome, ); if (parsed.target === "$terminal") { const status = parsed.terminalStatus; if (status === undefined) { pushIssue(sink, { code: "transition_terminal_status_missing", stepId, outcome, message: `${stepId}.${outcome} terminal transition requires terminalStatus`, }); continue; } if (!TERMINAL_STATUSES.has(status)) { pushIssue(sink, { code: "transition_terminal_status_invalid", stepId, outcome, message: `${stepId}.${outcome} terminalStatus is invalid`, }); continue; } compiled[outcome] = { to: "terminal", terminalStatus: status as KxmTerminalStatus, ...(maxTransitions !== undefined ? { maxTransitions } : {}), }; continue; } if (parsed.terminalStatus !== undefined) { pushIssue(sink, { code: "transition_terminal_status_unexpected", stepId, outcome, message: `${stepId}.${outcome} step target must not declare terminalStatus`, }); } if (!parsed.target || !stepIndex.has(parsed.target)) { pushIssue(sink, { code: "transition_target_unknown", stepId, outcome, message: `${stepId}.${outcome} references unknown target ${String(parsed.target)}`, }); continue; } const targetIndex = stepIndex.get(parsed.target)!; const back = targetIndex <= index; if (back && maxTransitions === undefined && parsed.maxTransitions === undefined) { pushIssue(sink, { code: "back_edge_unbounded", stepId, outcome, message: `${stepId}.${outcome} back-edge requires maxTransitions`, }); } compiled[outcome] = { to: "step", target: parsed.target, edge: back ? "back" : "forward", ...(maxTransitions !== undefined ? { maxTransitions } : {}), }; } return compiled; } function parseTransition(value: JsonValue): { target?: string; maxTransitions?: JsonValue; terminalStatus?: string } { if (typeof value === "string") return { target: value }; const object = objectValue(value); if (!object) return {}; const target = stringValue(object.target); const terminalStatus = stringValue(object.terminalStatus); return { ...(target ? { target } : {}), ...(object.maxTransitions !== undefined ? { maxTransitions: object.maxTransitions } : {}), ...(terminalStatus ? { terminalStatus } : {}), }; } function compileOracle( field: "reproOracle" | "planHash", oracle: JsonObject | undefined, steps: readonly KxmCompiledStep[], sink: IssueSink, ): { stageId: string; evidenceKey: string } | undefined { if (!oracle) return undefined; const stageId = stringValue(oracle.stageId); const evidenceKey = stringValue(oracle.evidenceKey); const step = stageId === undefined ? undefined : steps.find((candidate) => candidate.id === stageId); if (!stageId || !step) { pushIssue(sink, { code: "oracle_stage_unknown", message: `${field} references unknown stage ${String(stageId)}` }); return undefined; } if (!evidenceKey || !step.requiredEvidence.some((item) => item.key === evidenceKey)) { pushIssue(sink, { code: "oracle_evidence_unknown", message: `${field} references undeclared evidence ${String(evidenceKey)} in ${stageId}`, }); return undefined; } return { stageId, evidenceKey }; } function compileRepositories(value: JsonValue | undefined): Readonly<Record<string, KxmRepositoryAccess>> { const source = objectValue(value) ?? {}; const repositories: Record<string, KxmRepositoryAccess> = {}; for (const [id, access] of Object.entries(source)) { if (typeof access === "string" && REPOSITORY_ACCESS.has(access)) { repositories[id] = access as KxmRepositoryAccess; } } return repositories; } function compileSecrets(value: JsonValue | undefined): ReadonlyJsonObject[] { if (!Array.isArray(value)) return []; return value.map(objectValue).filter((entry): entry is JsonObject => Boolean(entry)).map(cloneJsonObject); } function compileCountField( value: JsonValue | undefined, fallback: number, path: string, stepId: string, sink: IssueSink, ): number { if (value === undefined) return fallback; if (isCount(value)) return value; pushIssue(sink, { code: "count_invalid", stepId, message: `${path} must be an integer >= 1` }); return fallback; } function compileOptionalCount( value: JsonValue | undefined, path: string, stepId: string, sink: IssueSink, outcome?: string, ): number | undefined { if (value === undefined) return undefined; if (isCount(value)) return value; pushIssue(sink, { code: "count_invalid", stepId, ...(outcome ? { outcome } : {}), message: `${path} must be an integer >= 1`, }); return undefined; } function compileOptionalDuration( value: JsonValue | undefined, path: string, stepId: string, sink: IssueSink, ): number | undefined { if (value === undefined) return undefined; if (isDuration(value)) return value; pushIssue(sink, { code: "duration_invalid", stepId, message: `${path} must be an integer >= 0` }); return undefined; } function isCount(value: JsonValue): value is number { return typeof value === "number" && Number.isInteger(value) && value >= 1; } function isDuration(value: JsonValue): value is number { return typeof value === "number" && Number.isInteger(value) && value >= 0; } function isCost(value: JsonValue): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; } function isStepId(value: string): boolean { return value.length >= 1 && value.length <= 64 && STEP_ID_PATTERN.test(value); } function isJsonObject(value: JsonValue | undefined): value is JsonObject { return value !== undefined && value !== null && typeof value === "object" && !Array.isArray(value); } function objectValue(value: JsonValue | undefined): JsonObject | undefined { return isJsonObject(value) ? value : undefined; } function stringValue(value: JsonValue | undefined): string | undefined { return typeof value === "string" ? value : undefined; } function valuesOf(object: JsonObject, field: string): JsonValue[] { const value = object[field]; return Array.isArray(value) ? value : []; } function names(value: JsonValue | undefined): string[] { return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : []; } function cloneJsonValue(value: JsonValue): ReadonlyJsonValue { if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value; if (Array.isArray(value)) return value.map((entry) => cloneJsonValue(entry)); return cloneJsonObject(isJsonObject(value) ? value : {}); } function cloneJsonObject(value: JsonObject): ReadonlyJsonObject { const copy: { [key: string]: ReadonlyJsonValue } = {}; for (const [key, child] of Object.entries(value)) copy[key] = cloneJsonValue(child); return copy; } function deepFreeze<T>(value: T): T { if (value === null || typeof value !== "object") return value; if (Array.isArray(value)) { for (const entry of value) deepFreeze(entry); } else { for (const entry of Object.values(value as Record<string, unknown>)) deepFreeze(entry); } return Object.freeze(value); } function compareCodeUnits(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function compareOptional(left: string | undefined, right: string | undefined): number { if (left === right) return 0; if (left === undefined) return -1; if (right === undefined) return 1; return compareCodeUnits(left, right); } function sortIssues(issues: readonly KxmEngineCompileIssue[]): KxmEngineCompileIssue[] { return [...issues].sort((left, right) => { const byStep = compareOptional(left.stepId, right.stepId); if (byStep !== 0) return byStep; const byOutcome = compareOptional(left.outcome, right.outcome); if (byOutcome !== 0) return byOutcome; return compareCodeUnits(left.code, right.code); }); } function pushIssue(sink: IssueSink, issue: Omit<KxmEngineCompileIssue, "workflowId">): void { sink.issues.push({ workflowId: sink.workflowId, ...issue }); }