|
| 1 | +export const RUNTIME_STATE_SCHEMA_VERSION = 1; |
| 2 | + |
| 3 | +export type RuntimeStatus = "draft" | "active" | "blocked" | "failed" | "sealed"; |
| 4 | +export type RuntimeTaskStatus = "pending" | "ready" | "claimed" | "verifying" | "completed" | "blocked" | "failed" | "repair_required"; |
| 5 | +export type RuntimeWorkerStatus = "ready" | "busy" | "unhealthy" | "completed-retained" | "safe-to-close" | "stale-registry"; |
| 6 | +export type RuntimeArtifactKind = "run-objective" | "policy-selection" | "task-graph" | "worker-state" | "evidence" | "evaluation" | "reward" | "integration-candidate" | "final-report"; |
| 7 | +export type EvaluationVerdict = "pass" | "fail" | "repair" | "blocked"; |
| 8 | +export type IntegrationCandidateStatus = "pending" | "accepted" | "rejected" | "repair_required"; |
| 9 | + |
| 10 | +export interface RuntimeArtifactRef { kind: RuntimeArtifactKind; artifactId: string; path: string } |
| 11 | +export interface RunObjectiveSchema { id: string; goal: string; successCriteria: string[]; constraints: string[] } |
| 12 | +export interface PolicySelectionSchema { id: string; selectedPolicyId: string; rationale: string; artifactRefs: RuntimeArtifactRef[] } |
| 13 | +export interface RuntimeTaskSchema { id: string; title: string; status: RuntimeTaskStatus; dependsOn: string[]; evidenceRefs: RuntimeArtifactRef[]; evaluationRefs: RuntimeArtifactRef[] } |
| 14 | +export interface RuntimeWorkerSchema { id: string; adapterId: string; status: RuntimeWorkerStatus; readinessNonce?: string; lastHeartbeatAt?: string } |
| 15 | +export interface EvaluationResultSchema { id: string; taskId?: string; verdict: EvaluationVerdict; evidenceRefs: RuntimeArtifactRef[] } |
| 16 | +export interface RewardRecordSchema { id: string; objectiveId: string; score: number; evidenceRefs: RuntimeArtifactRef[] } |
| 17 | +export interface IntegrationCandidateSchema { id: string; taskIds: string[]; status: IntegrationCandidateStatus; evidenceRefs: RuntimeArtifactRef[] } |
| 18 | + |
| 19 | +export interface RuntimeState { |
| 20 | + schemaVersion: typeof RUNTIME_STATE_SCHEMA_VERSION; |
| 21 | + runId: string; |
| 22 | + status: RuntimeStatus; |
| 23 | + createdAt: string; |
| 24 | + updatedAt: string; |
| 25 | + objective?: RunObjectiveSchema; |
| 26 | + policySelection?: PolicySelectionSchema; |
| 27 | + tasks: RuntimeTaskSchema[]; |
| 28 | + workers: RuntimeWorkerSchema[]; |
| 29 | + evaluations: EvaluationResultSchema[]; |
| 30 | + rewards: RewardRecordSchema[]; |
| 31 | + integrationCandidates: IntegrationCandidateSchema[]; |
| 32 | + artifactRefs: RuntimeArtifactRef[]; |
| 33 | +} |
| 34 | + |
| 35 | +export type RuntimeStateParseResult = { ok: true; state: RuntimeState } | { ok: false; reason: "malformed" | "unsupported-newer-schema"; issues: string[] }; |
| 36 | + |
| 37 | +const runtimeStatuses = new Set<RuntimeStatus>(["draft", "active", "blocked", "failed", "sealed"]); |
| 38 | +const taskStatuses = new Set<RuntimeTaskStatus>(["pending", "ready", "claimed", "verifying", "completed", "blocked", "failed", "repair_required"]); |
| 39 | +const workerStatuses = new Set<RuntimeWorkerStatus>(["ready", "busy", "unhealthy", "completed-retained", "safe-to-close", "stale-registry"]); |
| 40 | +const artifactKinds = new Set<RuntimeArtifactKind>(["run-objective", "policy-selection", "task-graph", "worker-state", "evidence", "evaluation", "reward", "integration-candidate", "final-report"]); |
| 41 | +const evaluationVerdicts = new Set<EvaluationVerdict>(["pass", "fail", "repair", "blocked"]); |
| 42 | +const integrationStatuses = new Set<IntegrationCandidateStatus>(["pending", "accepted", "rejected", "repair_required"]); |
| 43 | + |
| 44 | +export function createRuntimeState(input: { runId: string; now: string }): RuntimeState { |
| 45 | + needText(input.runId, "runId", []); |
| 46 | + if (!isTimestamp(input.now)) throw new Error("now must be a timestamp"); |
| 47 | + return { schemaVersion: 1, runId: input.runId, status: "draft", createdAt: input.now, updatedAt: input.now, tasks: [], workers: [], evaluations: [], rewards: [], integrationCandidates: [], artifactRefs: [] }; |
| 48 | +} |
| 49 | + |
| 50 | +export function parseRuntimeState(input: unknown): RuntimeStateParseResult { |
| 51 | + if (!isRecord(input)) return { ok: false, reason: "malformed", issues: ["runtime state must be an object"] }; |
| 52 | + if (typeof input.schemaVersion !== "number" || !Number.isInteger(input.schemaVersion) || input.schemaVersion < 1) return { ok: false, reason: "malformed", issues: ["schemaVersion must be a positive integer"] }; |
| 53 | + if (input.schemaVersion > RUNTIME_STATE_SCHEMA_VERSION) return { ok: false, reason: "unsupported-newer-schema", issues: [`unsupported runtime state schemaVersion ${input.schemaVersion}`] }; |
| 54 | + const issues: string[] = []; |
| 55 | + needText(input.runId, "runId", issues); |
| 56 | + needEnum(input.status, runtimeStatuses, "status", issues); |
| 57 | + needTimestamp(input.createdAt, "createdAt", issues); |
| 58 | + needTimestamp(input.updatedAt, "updatedAt", issues); |
| 59 | + if (input.objective !== undefined) validateObjective(input.objective, "objective", issues); |
| 60 | + if (input.policySelection !== undefined) validatePolicy(input.policySelection, "policySelection", issues); |
| 61 | + eachRecord(input.tasks, "tasks", issues, (item, label) => validateTask(item, label, issues)); |
| 62 | + eachRecord(input.workers, "workers", issues, (item, label) => validateWorker(item, label, issues)); |
| 63 | + eachRecord(input.evaluations, "evaluations", issues, (item, label) => validateEvaluation(item, label, issues)); |
| 64 | + eachRecord(input.rewards, "rewards", issues, (item, label) => validateReward(item, label, issues)); |
| 65 | + eachRecord(input.integrationCandidates, "integrationCandidates", issues, (item, label) => validateIntegration(item, label, issues)); |
| 66 | + eachRecord(input.artifactRefs, "artifactRefs", issues, (item, label) => validateArtifactRef(item, label, issues)); |
| 67 | + return issues.length ? { ok: false, reason: "malformed", issues } : { ok: true, state: input as unknown as RuntimeState }; |
| 68 | +} |
| 69 | + |
| 70 | +export function buildRunContractRuntimeRefs(state: RuntimeState): RuntimeArtifactRef[] { |
| 71 | + return state.artifactRefs.filter((ref) => ref.kind === "run-objective" || ref.kind === "policy-selection" || ref.kind === "evaluation"); |
| 72 | +} |
| 73 | + |
| 74 | +function validateObjective(value: unknown, label: string, issues: string[]): void { |
| 75 | + if (!isRecord(value)) { issues.push(`${label} must be an object`); return; } |
| 76 | + needText(value.id, `${label}.id`, issues); needText(value.goal, `${label}.goal`, issues); needStringArray(value.successCriteria, `${label}.successCriteria`, issues); needStringArray(value.constraints, `${label}.constraints`, issues); |
| 77 | +} |
| 78 | +function validatePolicy(value: unknown, label: string, issues: string[]): void { |
| 79 | + if (!isRecord(value)) { issues.push(`${label} must be an object`); return; } |
| 80 | + needText(value.id, `${label}.id`, issues); needText(value.selectedPolicyId, `${label}.selectedPolicyId`, issues); needText(value.rationale, `${label}.rationale`, issues); eachRecord(value.artifactRefs, `${label}.artifactRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); |
| 81 | +} |
| 82 | +function validateTask(value: Record<string, unknown>, label: string, issues: string[]): void { |
| 83 | + needText(value.id, `${label}.id`, issues); needText(value.title, `${label}.title`, issues); needEnum(value.status, taskStatuses, `${label}.status`, issues); needStringArray(value.dependsOn, `${label}.dependsOn`, issues); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); eachRecord(value.evaluationRefs, `${label}.evaluationRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); |
| 84 | +} |
| 85 | +function validateWorker(value: Record<string, unknown>, label: string, issues: string[]): void { |
| 86 | + needText(value.id, `${label}.id`, issues); needText(value.adapterId, `${label}.adapterId`, issues); needEnum(value.status, workerStatuses, `${label}.status`, issues); if (value.lastHeartbeatAt !== undefined) needTimestamp(value.lastHeartbeatAt, `${label}.lastHeartbeatAt`, issues); |
| 87 | +} |
| 88 | +function validateEvaluation(value: Record<string, unknown>, label: string, issues: string[]): void { |
| 89 | + needText(value.id, `${label}.id`, issues); needEnum(value.verdict, evaluationVerdicts, `${label}.verdict`, issues); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); |
| 90 | +} |
| 91 | +function validateReward(value: Record<string, unknown>, label: string, issues: string[]): void { |
| 92 | + needText(value.id, `${label}.id`, issues); needText(value.objectiveId, `${label}.objectiveId`, issues); if (typeof value.score !== "number" || !Number.isFinite(value.score)) issues.push(`${label}.score must be finite`); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); |
| 93 | +} |
| 94 | +function validateIntegration(value: Record<string, unknown>, label: string, issues: string[]): void { |
| 95 | + needText(value.id, `${label}.id`, issues); needStringArray(value.taskIds, `${label}.taskIds`, issues); needEnum(value.status, integrationStatuses, `${label}.status`, issues); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); |
| 96 | +} |
| 97 | +function validateArtifactRef(value: Record<string, unknown>, label: string, issues: string[]): void { |
| 98 | + needEnum(value.kind, artifactKinds, `${label}.kind`, issues); needText(value.artifactId, `${label}.artifactId`, issues); needText(value.path, `${label}.path`, issues); |
| 99 | +} |
| 100 | + |
| 101 | +function eachRecord(value: unknown, label: string, issues: string[], visit: (record: Record<string, unknown>, label: string) => void): void { |
| 102 | + if (!Array.isArray(value)) { issues.push(`${label} must be an array`); return; } |
| 103 | + value.forEach((item, index) => isRecord(item) ? visit(item, `${label}[${index}]`) : issues.push(`${label}[${index}] must be an object`)); |
| 104 | +} |
| 105 | +function needStringArray(value: unknown, label: string, issues: string[]): void { |
| 106 | + if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) issues.push(`${label} must be a string array`); |
| 107 | +} |
| 108 | +function needText(value: unknown, label: string, issues: string[]): void { |
| 109 | + if (typeof value !== "string" || !value.trim()) issues.push(`${label} is required`); |
| 110 | +} |
| 111 | +function needEnum<T extends string>(value: unknown, values: Set<T>, label: string, issues: string[]): void { |
| 112 | + if (typeof value !== "string" || !values.has(value as T)) issues.push(`${label} is invalid`); |
| 113 | +} |
| 114 | +function needTimestamp(value: unknown, label: string, issues: string[]): void { |
| 115 | + if (typeof value !== "string" || !isTimestamp(value)) issues.push(`${label} must be a timestamp`); |
| 116 | +} |
| 117 | +function isTimestamp(value: string): boolean { return Number.isFinite(Date.parse(value)); } |
| 118 | +function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); } |
0 commit comments