/** * Capability contract — the typed leaf behavior components compose from. * * A capability is a verb ("docker-build", "cfn-deploy", "wait-steady-state"), * never a noun (never named after the component that happens to use it). * Registered once by `kind`, dispatched by the orchestrator, composed by an * unbounded number of components. See: * https://intentius.io/chant/components/capabilities/ * * This module defines the interface and registry only. Verb implementations * live under `./verbs/*` as typed stubs — no cloud calls, no side effects. * Cloud implementations are a later phase (see epic #551, issue #554). */ /** * Ambient information a capability's `run`/`rollback` receives, independent of * its typed `input`. Deliberately minimal for this phase: the orchestrator * (interpret-mode driver, #556) will thread through the resolved environment, * a logger, and step-output wiring (`@Phase.output`) once it exists. Kept as * an extensible interface so a later phase can widen it without breaking the * `Capability` signature. */ export interface DeployContext { /** Target environment name (e.g. "dev", "staging", "prod"). */ env: string; /** Component name this run belongs to, for logging/attribution. */ component: string; /** Arbitrary environment config resolved by the orchestrator (registry URLs, cluster names, ...). */ vars?: Record; } /** * A typed leaf behavior. `kind` is the verb string components reference in * their composition (`{ kind: "cfn-deploy", ... }`); `run` performs the * operation; `rollback` is the optional paired compensation the orchestrator * calls, in reverse step order, on saga unwind. * * Typed `In`/`Out` let a composition wire one step's output into the next * step's input (`imageRef: "@Publish.digest"`) and let lint check the wiring * before anything runs. */ export interface Capability { /** The verb this capability implements — e.g. "docker-build", "cfn-deploy". Never a component name. */ readonly kind: string; /** Perform the operation. */ run(ctx: DeployContext, input: In): Promise; /** Optional paired compensation, invoked in reverse order on saga rollback. */ rollback?(ctx: DeployContext, input: In): Promise; /** * How this verb relates to rollback, for the COMP003 composition check. * Usually derivable and left unset: a capability with a `rollback` method is * treated as `"native"`, everything else as `"none-by-design"` (build/publish/ * wait — nothing to compensate). Set it explicitly to `"needs-opt-out"` on a * *mutating* verb that has no rollback and no safe undo (e.g. `s3-sync`, * `run-migration`), so COMP003 requires the component to acknowledge the * compensation gap. See ../lint/rules/comp/comp003-mutating-no-rollback.ts. */ readonly rollbackPolicy?: RollbackPolicy; } /** A capability's relationship to rollback — see `Capability.rollbackPolicy`. */ export type RollbackPolicy = "native" | "none-by-design" | "needs-opt-out"; /** Extract a capability's `In` type. */ export type CapabilityInput = C extends Capability ? In : never; /** Extract a capability's `Out` type. */ export type CapabilityOutput = C extends Capability ? Out : never; /** * Thrown by a stub `run`/`rollback` — the verb is specified and typed but has * no cloud implementation yet. Distinguishes "not implemented" from a runtime * failure so callers (and tests) can assert on it specifically. */ export class CapabilityNotImplementedError extends Error { constructor(public readonly kind: string) { super(`capability "${kind}" is not implemented`); this.name = "CapabilityNotImplementedError"; } } /** * Resolves capabilities by `kind`. One registry instance is the composition * root the orchestrator dispatches through; `createCapabilityRegistry` builds * one pre-seeded with the starter verb set (see `./verbs`). */ export class CapabilityRegistry { private readonly capabilities = new Map>(); /** Register a capability. Throws if `kind` is already registered — a capability is a verb, registered once. */ register(capability: Capability): this { if (this.capabilities.has(capability.kind)) { throw new Error(`capability "${capability.kind}" is already registered`); } this.capabilities.set(capability.kind, capability as Capability); return this; } /** Resolve a capability by `kind`. Throws a friendly error listing known kinds if absent. */ resolve(kind: string): Capability { const capability = this.capabilities.get(kind); if (!capability) { const known = [...this.capabilities.keys()].sort().join(", "); throw new Error(`no capability registered for kind "${kind}" (known: ${known})`); } return capability; } /** True if a capability is registered for `kind`. */ has(kind: string): boolean { return this.capabilities.has(kind); } /** All registered kinds, sorted. */ kinds(): string[] { return [...this.capabilities.keys()].sort(); } }