Diagnostics
For @arki/[email protected] · source docs/diagnostics.md
DOT exposes two parallel observability surfaces:
app.manifest— the static graph (what the app is).app.diagnostics— the runtime snapshot (how the app is doing).
Both are first-class data contracts, designed to be consumed by humans and agents.
DotAppManifest
The manifest is the declarative description of an app. It is finalised once
configure() completes and never changes afterwards.
import type { DotAppManifest as ExportedManifest } from '@arki/dot';
type DotAppManifest = {
manifestVersion: 2;
app: { name: string; version?: string };
plugins: PluginManifest[];
actions: ActionManifest[];
services: ServiceManifest[];
lifecycle: LifecycleManifest[];
dependencies: DependencyEdge[];
projections: ProjectionManifest[];
};
// Shapes are written out to read. This line stops compiling if one drifts from the exported type.
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
export const manifestMatches: Same<DotAppManifest, ExportedManifest> = true;
// The sub-shapes are in the next block.
type PluginManifest = ExportedManifest['plugins'][number];
type ActionManifest = ExportedManifest['actions'][number];
type ServiceManifest = ExportedManifest['services'][number];
type LifecycleManifest = ExportedManifest['lifecycle'][number];
type DependencyEdge = ExportedManifest['dependencies'][number];
type ProjectionManifest = ExportedManifest['projections'][number];Six-array contract. The six top-level arrays (plugins, actions,
services, lifecycle, dependencies, projections) are always
present, even when empty. Consumers must never see an omitted array.
Sub-shapes
import type { DotAppManifest, DotLifecycleHook, JsonObject } from '@arki/dot';
type PluginManifest = {
name: string;
role?: 'feature' | 'driving'; // the hexagon role the app builder gave the plugin
dependencies: readonly string[];
provides: readonly string[];
};
type ActionManifest = {
id: string;
plugin: string;
binding: string;
direction: 'in' | 'out';
address?: string;
summary?: string;
meta?: JsonObject;
metaSchema?: string;
};
type ServiceManifest = {
name: string;
plugin: string;
kind: 'env' | 'db' | 'kv' | 'queue' | 'auth' | 'email' | 'logger' | 'event-store' | 'message-bus' | 'custom';
};
type LifecycleManifest = {
plugin: string;
hooks: readonly DotLifecycleHook[];
};
type DependencyEdge = {
from: string;
to: string;
kind: 'requires' | 'provides' | 'uses';
key?: string; // the capability, on edges derived at configure() from static provides
};
type ProjectionManifest = {
format: string;
binding: string;
module: string;
plugin: string;
};
// Each shape must equal the entry type of the exported manifest.
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
export const pluginMatches: Same<PluginManifest, DotAppManifest['plugins'][number]> = true;
export const actionMatches: Same<ActionManifest, DotAppManifest['actions'][number]> = true;
export const serviceMatches: Same<ServiceManifest, DotAppManifest['services'][number]> = true;
export const lifecycleMatches: Same<LifecycleManifest, DotAppManifest['lifecycle'][number]> = true;
export const edgeMatches: Same<DependencyEdge, DotAppManifest['dependencies'][number]> = true;
export const projectionMatches: Same<ProjectionManifest, DotAppManifest['projections'][number]> = true;DotDiagnosticsSnapshot
A diagnostics snapshot is a point-in-time record. Reading
app.diagnostics re-computes it from the kernel's live state.
import type { DotLifecycleState, DotDiagnosticsSnapshot as ExportedSnapshot } from '@arki/dot';
type DotDiagnosticsSnapshot = {
snapshotVersion: 2;
generatedAt: string; // ISO-8601
app: { name: string; state: DotLifecycleState };
plugins: ExportedSnapshot['plugins'];
actions: ExportedSnapshot['actions'];
services: ExportedSnapshot['services'];
lifecycle: ExportedSnapshot['lifecycle'];
issues: ExportedSnapshot['issues'];
};
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
export const snapshotMatches: Same<DotDiagnosticsSnapshot, ExportedSnapshot> = true;Five-array contract. Same rule as the manifest — plugins, actions,
services, lifecycle, issues are always present.
DiagnosticIssue
The single actionable record:
import type { DiagnosticIssue as ExportedIssue } from '@arki/dot';
type DiagnosticIssue = {
code: string; // stable machine-readable code, e.g. ARKI_DOT_E003
severity: 'info' | 'warning' | 'error';
plugin?: string; // owning plugin, when applicable
message: string; // human-readable summary
remediation: string; // what the developer (or agent) should do
docsUrl: string; // link to the relevant doc
metadata?: Record<string, unknown>;
};
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
export const issueMatches: Same<DiagnosticIssue, ExportedIssue> = true;Every issue is required to carry code, message, remediation, and
docsUrl. Diagnostics are never "something went wrong" — they are always
actionable.
Programmatic access
import { defineApp, plugin } from '@arki/dot';
import { isFailure } from '@arki/failure';
const database = plugin({ name: 'database', provides: { db: () => ({ ready: true }) } });
const billing = plugin({ name: 'billing', needs: { db: database.provides.db } });
const app = await defineApp('my-app').use(database).use(billing).boot();
if (isFailure(app)) throw new Error(app.code);
// Static graph
console.log(app.manifest.plugins.map(p => p.name).join());
console.log(JSON.stringify(app.manifest.dependencies));
// Runtime snapshot
const snapshot = app.diagnostics;
console.log(snapshot.app.state, snapshot.issues.length);
for (const issue of snapshot.issues) {
console.error(`[${issue.severity}] ${issue.code}: ${issue.message}`);
console.error(` remediation: ${issue.remediation}`);
console.error(` docs: ${issue.docsUrl}`);
}
await app.dispose();database,billing
[{"from":"billing","to":"database","kind":"requires","key":"db"}]
booted 0app.diagnostics is re-computed on every access, so reading it inside a
healthcheck loop is fine — no caching required by the caller.
CLI: dot explain
dot explain prints the manifest:
# Human-readable
npx dot explain
# JSON (for agents and CI)
npx dot explain --json | jq '.data.manifest.plugins'The CLI does not boot the app — it only runs configure. This means
dot explain is safe on a misconfigured app: it surfaces what was declared
without trying to open resources. It prints one labelled edge per
capability, such as daily-digest --[db]--> db. Manifest dependencies
entries carry key, and manifest provides is filled at configure(),
adapters included.
Wiring codes
The kernel checks every plugin's needs against every plugin's static provides
at configure():
ARKI_DOT_E020— a provider is mounted after its consumer. The fix line is role-aware: "Move X before Y in .features([...])." for a feature provider, "Mount X with .use() before Y." for any other plugin.ARKI_DOT_E019— needs and provides form a ring through any plugins. The message prints the ring.ARKI_DOT_E012— a need at boot that no plugin declares (erased or JS graphs, dynamic http bundle tokens). It is no longer the code for a plain ordering mistake.
Output envelope (arki.cli/1, from @arki/cli; JSON when piped or with --json):
import type { Envelope } from '@arki/cli';
import type { DotAppManifest } from '@arki/dot';
// What `dot explain --json` prints, read the way an agent reads it.
export function pluginNames(envelope: Envelope<DotAppManifest>): readonly string[] {
if (!envelope.ok) throw new Error(`${envelope.error.code}: ${envelope.error.remediation ?? envelope.error.message}`);
return envelope.data?.plugins.map(p => p.name) ?? [];
}.data is the DotAppManifest itself. When configure returns a failure, the envelope
is ok: false with the canonical failure code and exit 3. Private causes do
not serialize: an UnexpectedDefect has null public data. A compatibility
callback that throws can still produce dot.app_lifecycle_failed.
CLI: dot doctor
dot doctor boots the app, captures app.diagnostics, then disposes:
# Human-readable
npx dot doctor
# JSON (for agents and CI)
npx dot doctor --json | jq '.data.issues'If a managed boot or disposal phase fails, dot doctor still emits a diagnostics envelope
populated with the per-plugin issues from the configured seam. Boot failure
should be observable, not opaque.
Exit codes:
0—successorwarningenvelope (warnings still mean the app booted).1—failureenvelope or a structured CLI error (bad args, app not found, etc.).
App discovery
Both commands auto-discover the app by looking for, in order:
--app <path>if passed../dot.config.ts./src/app.ts./app.ts
The discovered file must export an app builder, configured app, or booted
app as the default export or as app.
See also
- Lifecycle — error codes and state transitions.
- Agent guide — using
--jsonoutput from a coding agent.