import type { ErrorCode } from "./error"; export const CAPABILITY_CONTRACT_SCHEMA_VERSION = 1 as const; export const CAPABILITY_CONTRACT_ID = "workflow:freeze_capability_error_budget_contract" as const; export const CAPABILITY_CONTRACT_ERROR_CODES = [ "PROTOCOL_MISMATCH", "REVISION_CONFLICT", "OPEN_CANCELLED", "CAPABILITY_CONTRACT_INVALID", "CAPABILITY_CONTRACT_BUDGET_EXCEEDED", "CAPABILITY_CONTRACT_UNSUPPORTED", "CAPABILITY_CONTRACT_SCHEMA_HASH_MISMATCH", "CAPABILITY_CONTRACT_MIGRATION_BLOCKED", ] as const satisfies readonly ErrorCode[]; export type CapabilityContractErrorCode = typeof CAPABILITY_CONTRACT_ERROR_CODES[number]; export interface CapabilityContractError { code: CapabilityContractErrorCode; severity: "info" | "warning" | "error" | "fatal"; recoverable: boolean; } export interface CapabilityContractIR { schemaVersion: typeof CAPABILITY_CONTRACT_SCHEMA_VERSION; contractId: typeof CAPABILITY_CONTRACT_ID; capability: { level: "L0" | "L1" | "L2"; status: "BOUNDED" | "READY" | "BLOCKED" }; revision: { base: number; current: number }; budget: { maxErrors: number; maxMessageBytes: number; maxUnknownFields: number }; errors: CapabilityContractError[]; cancellation: { code: "OPEN_CANCELLED"; preservesRevision: boolean }; migration: { currentSchemaVersion: 1; supportedSchemaVersions: number[]; preserveUnknown: boolean; strategy: "RETAIN_UNKNOWN" }; schemaHash: string; extensions?: Record; unknownFields?: Record; } export class CapabilityContractValidationError extends Error { readonly code: CapabilityContractErrorCode; constructor(code: CapabilityContractErrorCode, message: string) { super(`${code}: ${message}`); this.name = "CapabilityContractValidationError"; this.code = code; } } function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function integer(value: unknown, name: string, minimum: number, maximum: number): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `${name} is outside its bounded range`); } return value; } function text(value: unknown, name: string, maximum: number): string { if (typeof value !== "string" || value.length === 0 || value.length > maximum) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `${name} is invalid`); } return value; } function digest(value: unknown, name: string): string { const result = text(value, name, 64); if (!/^[a-f0-9]{64}$/u.test(result)) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_SCHEMA_HASH_MISMATCH", `${name} is not a SHA-256 digest`); } return result; } export function parseCapabilityContract(value: unknown): CapabilityContractIR { if (!record(value)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "contract must be an object"); const input = value; if (input.schemaVersion !== CAPABILITY_CONTRACT_SCHEMA_VERSION) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_UNSUPPORTED", "unsupported schema version"); } if (input.contractId !== CAPABILITY_CONTRACT_ID) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "contractId does not identify this workflow"); } if (!record(input.capability) || !["L0", "L1", "L2"].includes(input.capability.level as string) || !["BOUNDED", "READY", "BLOCKED"].includes(input.capability.status as string)) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "capability is invalid"); } if (!record(input.revision)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "revision is invalid"); const revision = { base: integer(input.revision.base, "revision.base", 0, Number.MAX_SAFE_INTEGER), current: integer(input.revision.current, "revision.current", 0, Number.MAX_SAFE_INTEGER), }; if (revision.current < revision.base) throw new CapabilityContractValidationError("REVISION_CONFLICT", "current revision precedes base revision"); if (!record(input.budget)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "budget is invalid"); const budget = { maxErrors: integer(input.budget.maxErrors, "budget.maxErrors", 1, 128), maxMessageBytes: integer(input.budget.maxMessageBytes, "budget.maxMessageBytes", 32, 4096), maxUnknownFields: integer(input.budget.maxUnknownFields, "budget.maxUnknownFields", 0, 128), }; if (!Array.isArray(input.errors) || input.errors.length === 0 || input.errors.length > budget.maxErrors) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_BUDGET_EXCEEDED", "errors exceed the declared budget"); } const errorCodes = new Set(); const errors = input.errors.map((entry, index) => { if (!record(entry) || !CAPABILITY_CONTRACT_ERROR_CODES.includes(entry.code as CapabilityContractErrorCode) || errorCodes.has(entry.code as string)) { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `errors[${index}] is invalid or duplicated`); } errorCodes.add(entry.code as string); if (!["info", "warning", "error", "fatal"].includes(entry.severity as string) || typeof entry.recoverable !== "boolean") { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `errors[${index}] has invalid severity or recovery policy`); } return { code: entry.code as CapabilityContractErrorCode, severity: entry.severity as CapabilityContractError["severity"], recoverable: entry.recoverable }; }); if (!record(input.cancellation) || input.cancellation.code !== "OPEN_CANCELLED" || typeof input.cancellation.preservesRevision !== "boolean") { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "cancellation policy is invalid"); } if (!record(input.migration) || input.migration.currentSchemaVersion !== 1 || !Array.isArray(input.migration.supportedSchemaVersions) || !input.migration.supportedSchemaVersions.includes(1) || input.migration.preserveUnknown !== true || input.migration.strategy !== "RETAIN_UNKNOWN") { throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_MIGRATION_BLOCKED", "migration policy must retain unknown data"); } const schemaHash = digest(input.schemaHash, "schemaHash"); if (input.extensions !== undefined && !record(input.extensions)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "extensions must be an object"); const extensions = input.extensions as Record | undefined; const knownFields = new Set(["schemaVersion", "contractId", "capability", "revision", "budget", "errors", "cancellation", "migration", "schemaHash", "extensions"]); const unknownFields = Object.fromEntries(Object.entries(input).filter(([key]) => !knownFields.has(key))); if (Object.keys(unknownFields).length > budget.maxUnknownFields) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_BUDGET_EXCEEDED", "unknown fields exceed the migration budget"); return { schemaVersion: 1, contractId: CAPABILITY_CONTRACT_ID, capability: { level: input.capability.level as "L0" | "L1" | "L2", status: input.capability.status as "BOUNDED" | "READY" | "BLOCKED" }, revision, budget, errors, cancellation: { code: "OPEN_CANCELLED", preservesRevision: input.cancellation.preservesRevision }, migration: { currentSchemaVersion: 1, supportedSchemaVersions: input.migration.supportedSchemaVersions.map((value) => integer(value, "migration.supportedSchemaVersions[]", 1, 1)), preserveUnknown: true, strategy: "RETAIN_UNKNOWN" }, schemaHash, ...(extensions === undefined ? {} : { extensions }), ...(Object.keys(unknownFields).length === 0 ? {} : { unknownFields }), }; } function stable(value: unknown): unknown { if (Array.isArray(value)) return value.map(stable); if (!record(value)) return value; return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])); } /** Canonical bytes covered by schemaHash; unknown extension data is retained. */ export function capabilityContractSchemaInput(value: CapabilityContractIR): string { const { schemaHash: _schemaHash, unknownFields: _unknownFields, ...payload } = value; return JSON.stringify(stable(payload)); } /** Version-1 migration is intentionally lossless for unknown root fields/extensions. */ export function migrateCapabilityContract(value: unknown): CapabilityContractIR { return parseCapabilityContract(value); }