import { Schema } from "effect" import { err, type ToolErr } from "./result.ts" /** No `.apnea/state.json` for the current project root. */ export class NoRunState extends Schema.TaggedError()( "NoRunState", {}, ) {} /** Tool call refused by the step → legal-tools table. */ export class IllegalTool extends Schema.TaggedError()( "IllegalTool", { step: Schema.String, tool: Schema.String, legal: Schema.Array(Schema.String), }, ) {} /** Dispatch kind refused at the current step. */ export class IllegalKind extends Schema.TaggedError()( "IllegalKind", { step: Schema.String, kind: Schema.String, allowed: Schema.Array(Schema.String), }, ) {} /** Global or project config missing, invalid, or untrusted. */ export class ConfigError extends Schema.TaggedError()( "ConfigError", { message: Schema.String, path: Schema.optional(Schema.String), details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }, ) {} /** `state.json` present but not decodable / inconsistent. */ export class StateCorrupt extends Schema.TaggedError()( "StateCorrupt", { path: Schema.String, message: Schema.String, }, ) {} /** VCS detect / dirty / commit / bookmark failure. */ export class VcsError extends Schema.TaggedError()("VcsError", { message: Schema.String, command: Schema.optional(Schema.String), }) {} /** Another live Apnea process owns the repository mutation lock. */ export class OperationLocked extends Schema.TaggedError()( "OperationLocked", { message: Schema.String, repository: Schema.String, lock_path: Schema.String, reason: Schema.String, pid: Schema.Number, }, ) {} /** Herdr CLI or pane failure. */ export class HerdrError extends Schema.TaggedError()("HerdrError", { message: Schema.String, command: Schema.optional(Schema.String), details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} /** Commit/review gate refused (e.g. verdict not APPROVED). */ export class GateRefused extends Schema.TaggedError()( "GateRefused", { gate: Schema.String, message: Schema.String, details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }, ) {} /** `workflow_wait` hit its timeout without a complete artifact. */ export class WaitTimeout extends Schema.TaggedError()( "WaitTimeout", { artifact: Schema.String, timeoutMs: Schema.Number, details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }, ) {} /** `workflow_wait` aborted (Esc / cancel signal). */ export class WaitAborted extends Schema.TaggedError()( "WaitAborted", { artifact: Schema.String, details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }, ) {} /** A non-wait operation was interrupted by its host cancellation signal. */ export class OperationAborted extends Schema.TaggedError()( "OperationAborted", { operation: Schema.String, details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }, ) {} /** Artifact exists but front-matter / shape is invalid. */ export class ArtifactInvalid extends Schema.TaggedError()( "ArtifactInvalid", { artifact: Schema.String, message: Schema.String, }, ) {} /** Phase-package verify commands failed — commit refused. */ export class VerifyFailed extends Schema.TaggedError()( "VerifyFailed", { commands: Schema.Array(Schema.String), /** Tail of the verify log — truncated; `verify_log` has the full text. */ outputs: Schema.Array(Schema.String), /** Repo-relative path of the full verify log. */ verify_log: Schema.String, }, ) {} export type AppError = | NoRunState | IllegalTool | IllegalKind | ConfigError | StateCorrupt | VcsError | OperationLocked | HerdrError | GateRefused | WaitTimeout | WaitAborted | OperationAborted | ArtifactInvalid | VerifyFailed const APP_ERROR_TAG_LIST = [ "NoRunState", "IllegalTool", "IllegalKind", "ConfigError", "StateCorrupt", "VcsError", "OperationLocked", "HerdrError", "GateRefused", "WaitTimeout", "WaitAborted", "OperationAborted", "ArtifactInvalid", "VerifyFailed", ] as const satisfies readonly AppError["_tag"][] /** * Compile-time guard: an `AppError` member missing from `APP_ERROR_TAG_LIST` * would make `isAppError` return false for it, silently degrading a designed * refusal into `bug: …`. Unlike the `toToolResult` switch, a `Set` * cannot be checked by exhaustiveness alone — so assert it here. */ type AssertNever = T type _AllAppErrorTagsCovered = AssertNever< Exclude > const APP_ERROR_TAGS: ReadonlySet = new Set(APP_ERROR_TAG_LIST) export function isAppError(u: unknown): u is AppError { return ( typeof u === "object" && u !== null && "_tag" in u && typeof (u as { _tag: unknown })._tag === "string" && APP_ERROR_TAGS.has((u as { _tag: string })._tag) ) } /** Map a tagged app error to the stable ToolErr boundary shape. */ export function toToolResult(e: AppError): ToolErr { switch (e._tag) { case "NoRunState": return err("no run state; call workflow_start first", { legal_next: ["workflow_start"], }) case "IllegalTool": return err( `illegal tool ${e.tool} at step=${e.step}. legal: ${e.legal.join(", ") || "(none)"}`, { legal_next: [...e.legal], data: { step: e.step, tool: e.tool }, }, ) case "IllegalKind": return err( `kind=${e.kind} not allowed at step=${e.step}. allowed: ${e.allowed.join(", ")}`, { legal_next: ["dispatch_role with allowed kind", "workflow_status"], data: { step: e.step, kind: e.kind, allowed: e.allowed }, }, ) case "ConfigError": { const data = { ...(e.path !== undefined ? { path: e.path } : {}), ...(e.details ?? {}), } return err(e.message, { data: Object.keys(data).length > 0 ? data : undefined, }) } case "StateCorrupt": return err(`corrupt state at ${e.path}: ${e.message}`, { data: { path: e.path }, }) case "VcsError": return err(e.message, { data: e.command !== undefined ? { command: e.command } : undefined, }) case "OperationLocked": return err(e.message, { data: { repository: e.repository, lock_path: e.lock_path, reason: e.reason, pid: e.pid, }, }) case "HerdrError": return err(e.message, { data: e.command !== undefined || e.details !== undefined ? { ...(e.command !== undefined ? { command: e.command } : {}), ...(e.details ?? {}), } : undefined, }) case "GateRefused": return err(e.message, { data: { gate: e.gate, ...(e.details ?? {}), }, }) case "WaitTimeout": return err(`timeout after ${e.timeoutMs}ms waiting for ${e.artifact}`, { data: { artifact: e.artifact, timeout_ms: e.timeoutMs, ...(e.details ?? {}), }, }) case "WaitAborted": return err("workflow_wait aborted (Esc / cancel)", { data: { artifact: e.artifact, ...(e.details ?? {}) }, }) case "OperationAborted": return err(`${e.operation} aborted (signal / cancel)`, { data: { operation: e.operation, ...(e.details ?? {}) }, }) case "ArtifactInvalid": return err(e.message, { data: { artifact: e.artifact } }) case "VerifyFailed": return err("verify commands failed — commit refused", { data: { commands: e.commands, outputs: e.outputs, verify_log: e.verify_log, }, }) } }