Upgrade typed phases

For @arki/[email protected] · source docs/upgrade-phases.md

This is a breaking minor release while Dot is pre-1.0. Rebuild @arki/dot and packages that emit its types, then typecheck consumers. The lifetime migration in Upgrade owned lifetimes still applies.

Check configure before reading the manifest

configure() now returns a configured app or its inferred configure refusal or canonical UnexpectedDefect. It remains synchronous and performs no I/O. Replace direct .configure().manifest access with a failure check:

import { defineApp, plugin } from '@arki/dot';
import { isFailure } from '@arki/failure';
 
const catalog = plugin({ name: 'catalog', provides: { catalog: () => ({ ready: true }) } });
const configured = defineApp('shop').use(catalog).configure();
if (isFailure(configured)) throw new Error(configured.code); // Example's process boundary.
const manifest = configured.manifest;
if (manifest.app.name !== 'shop') throw new Error('wrong manifest');
const app = await configured.boot();
if (isFailure(app)) throw new Error(app.code);
const disposed = await app.dispose();
if (isFailure(disposed)) throw new Error(disposed.code);

Inside application code, return the failure unchanged. At a process or protocol boundary, translate it into an unsuccessful exit or safe response. Tests and an explicit exception boundary may use orThrow; a wrapper that throws every returned failure erases the contract.

Keep each phase's outcomes

The compiler infers refusals from the actual callbacks across plugin, object feature, defineFeature, initPlugins, driver, rename and app builder forms. Avoid annotating them with a broad Failure return or a manually restated union. Providers expose only their successful service values. Failed providers never enter the registry; each completed plugin publishes its services atomically.

OperationSuccessFailures reachable from this entry point
Builder configure()Configured appConfigure refusals and UnexpectedDefect.
Builder boot()Booted appConfigure outcomes, then provider/usecase construction and boot outcomes; rollback can add disposal failures in an aggregate.
Configured boot()Booted appProvider/usecase construction and boot outcomes; configure callbacks do not run again.
Builder start()Started appConfigure and boot outcomes, then start refusals and supervisor outcomes; rollback can add stop/disposal failures.
Configured start()Started appBoot and start outcomes; configure callbacks do not run again.
Booted app start()undefinedStart refusals and supervisor outcomes; rollback can add stop/disposal failures. Provider and configure refusals are excluded.
App stop()undefinedStop refusals and supervisor outcomes, aggregated when several fail.
App dispose()undefinedStop/dispose refusals, owned cleanup failures and supervisor outcomes, aggregated when several fail. Configure/start refusals are excluded.

Supervisor outcomes include canonical defects and owned cancellation/deadline outcomes where that work can reach them. Do not replace the inferred types with one application-wide failure union.

If startup fails, Dot joins owned work and rolls back resources before returning. Successful rollback returns the primary failure unchanged. Failed rollback returns an inferred ScopeAggregate: local cause.primary and cause.cleanup retain the ordered failures; public data contains safe counts. Private causes do not survive serialization. Stop and dispose continue every finalizer and return managed failures. Neither throws a managed refusal or hook defect.

Same-transition calls coalesce; different transitions remain serialized. Failed and disposed apps require a fresh builder. Disposal returns its cached outcome. createAppRuntime never caches a failed start as ready, and failed prior disposal blocks replacement ownership until release is resolved.

Author hooks with concrete returns

Configure, start, stop and dispose hooks return undefined or a concrete typed failure; async hooks resolve the same values. Boot additionally accepts ownership plans. Ordinary inline empty sync and async hooks remain valid. Detached callbacks typed as void or Promise<void> can hide a returned failure and are rejected: author the callback inline, or explicitly return undefined from the detached function. A configure callback must never return a Promise. Mounting also checks spread replacements: boot callbacks retain concrete outcomes and valid ownership plans, and replacement factories preserve the successful shapes carried by their handles. Adapter functions must preserve their inferred plugin returns. For overloads, derive each return with ReturnType from its implementation helper; a broad Plugin annotation discards the actual callback contracts.

Unknown or broad Failure factory results cannot prove an exact refusal union and are rejected. Keep factory returns inferred, or validate untyped external input with the canonical source schema before returning it.

Separate usecase creation from invocation

A .usecases(...) factory refusal fails construction and is part of boot's outcome. A returned usecase's business refusal belongs to invocation, rather than boot. Invocation preserves that refusal's identity and adds UnexpectedDefect for unknown throws or native Promise rejections, even with tracing disabled. Known refusals are reported without manufacturing exception events; a broken telemetry reporter cannot replace or recursively report the primary failure. Dependent generic call signatures and overload sets cannot preserve their return relationships through supervision; unsupported signatures are diagnosed. Expose concrete supported call signatures instead of casting around the diagnostic. Invocation returns must also be concrete: void, Promise<void> and unknown can erase a business refusal. For a usecase with no result, return undefined explicitly; validate external values before returning them.

Move activation and diagnostics to their boundaries

Logging activation now happens during boot. Configure and dot explain never activate the logging slot or evaluate its configuration thunk. Hook OTel context is active while hook work runs; scope versions come from package metadata.

Legacy ARKI_DOT_E* IDs remain stable. Kernel-generated lifecycle defects retain a private diagnostic cause with phase, owner and original cause. Owned provider/boot task defects retain their original private cause; diagnostics record the phase, owner and legacy ID. Match top-level failures by canonical definition. Static declarations outside a managed boundary can still throw. Managed CLI module loading catches module-evaluation defects; an unmanaged static import does not gain that boundary.

CLI consumers must inspect resolved failures. A failed managed dot _preflight --json report exits 3 with dot.preflight_failed; its full payload moves to .error.data.{app,migrations,report,failure}. Successful reports keep their existing .data shape. dot dev consumes the failure envelope and starts no runner. Doctor also treats failed disposal as an unsuccessful check.

Request, job and durable workflow adapter migrations remain separate work; this release does not claim those integrations are complete.

Bridge existing exception-based adapters explicitly

At legacy oRPC or HTTP handlers whose contracts do not yet declare canonical failures, use orThrow(await usecase(...)) at that boundary. Existing declared domain failures still use orThrowRpc. Internal calls check isFailure and return the same value. A Drizzle transaction callback also needs an explicit exception bridge until its returned-failure rollback migration lands in T06. Do not hide this choice inside a proxy or a global unwrapping helper.

Cron oRPC routes send UnexpectedDefect to their existing failed policy before success mapping. Ordinary successful values need no extra responder; declared business refusals still keep their explicit mapping.

Custom logging slots infer failures from both activation and close(). Check those outcomes through app boot/disposal. When adapting a foreign close(): Promise<void> API, use an inline async close method that awaits it; the method's completed result is explicitly empty. Returned close failures are preserved in the cleanup outcome, even when a reporting sink also throws.

All docs for @arki/[email protected]