@arki/dot 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kernel/app-instance.d.ts +1 -1
- package/dist/kernel/app-instance.d.ts.map +1 -1
- package/dist/kernel/app-instance.js +131 -23
- package/dist/kernel/app-instance.js.map +1 -1
- package/dist/lifecycle.d.ts +2 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +2 -0
- package/dist/lifecycle.js.map +1 -1
- package/dist/pip-contract.d.ts +15 -2
- package/dist/pip-contract.d.ts.map +1 -1
- package/dist/pip-contract.js.map +1 -1
- package/package.json +10 -4
- package/src/cli/discover.ts +223 -0
- package/src/cli/error-codes.ts +74 -0
- package/src/cli/files.ts +120 -0
- package/src/cli/index.ts +539 -0
- package/src/cli/json.ts +49 -0
- package/src/cli/new.ts +420 -0
- package/src/cli/observability-probe.ts +51 -0
- package/src/cli/render-doctor.ts +199 -0
- package/src/cli/render-explain.ts +161 -0
- package/src/define-app.ts +310 -0
- package/src/diagnostics.ts +91 -0
- package/src/index.ts +89 -0
- package/src/kernel/app-instance.ts +1452 -0
- package/src/kernel/otel.ts +265 -0
- package/src/lifecycle-observer.ts +100 -0
- package/src/lifecycle.ts +123 -0
- package/src/manifest.ts +94 -0
- package/src/pip-contract.ts +494 -0
- package/src/pip.ts +84 -0
- package/src/test-harness.ts +72 -0
- package/src/timeline.ts +137 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderers for `dot explain`.
|
|
3
|
+
*
|
|
4
|
+
* Reads a configured (or booted) app's `manifest` and emits one of:
|
|
5
|
+
* - JSON envelope to stdout (when --json)
|
|
6
|
+
* - human-readable plain text to stdout (default)
|
|
7
|
+
*
|
|
8
|
+
* The JSON envelope shape matches the broader release-tooling envelope so
|
|
9
|
+
* agents can parse the output identically across CLI surfaces.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DiagnosticIssue } from '../diagnostics.js';
|
|
13
|
+
import type { DotAppManifest } from '../manifest.js';
|
|
14
|
+
|
|
15
|
+
export type DotCliEnvelopeStatus = 'success' | 'failure' | 'warning';
|
|
16
|
+
|
|
17
|
+
export type DotCliEnvelope<T = unknown> = {
|
|
18
|
+
status: DotCliEnvelopeStatus;
|
|
19
|
+
command: string;
|
|
20
|
+
generatedAt: string;
|
|
21
|
+
data: T;
|
|
22
|
+
errors: DiagnosticIssue[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ExplainSource = {
|
|
26
|
+
/** The static manifest produced by configure (or read off a booted app). */
|
|
27
|
+
manifest: DotAppManifest;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RenderOptions = {
|
|
31
|
+
/** Set to true to emit JSON to stdout; otherwise pretty text. */
|
|
32
|
+
json: boolean;
|
|
33
|
+
/** Override clock for deterministic test output. */
|
|
34
|
+
now?: () => Date;
|
|
35
|
+
/** Override stdout sink. Defaults to `process.stdout.write`. */
|
|
36
|
+
out?: (line: string) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const defaultOut = (line: string) => {
|
|
40
|
+
process.stdout.write(line);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function nowIso(opts: RenderOptions): string {
|
|
44
|
+
const factory = opts.now ?? (() => new Date());
|
|
45
|
+
return factory().toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the envelope without writing anything. Useful for tests that need
|
|
50
|
+
* to assert shape and for embedding the CLI logic from other tools.
|
|
51
|
+
*/
|
|
52
|
+
export function buildExplainEnvelope(source: ExplainSource, opts: RenderOptions): DotCliEnvelope<DotAppManifest> {
|
|
53
|
+
return {
|
|
54
|
+
status: 'success',
|
|
55
|
+
command: 'explain',
|
|
56
|
+
generatedAt: nowIso(opts),
|
|
57
|
+
data: source.manifest,
|
|
58
|
+
errors: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pad(s: string, width: number): string {
|
|
63
|
+
return s.length >= width ? s : s + ' '.repeat(width - s.length);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderTextManifest(manifest: DotAppManifest): string {
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
const title = `App: ${manifest.app.name}${manifest.app.version ? `@${manifest.app.version}` : ''}`;
|
|
69
|
+
lines.push(title);
|
|
70
|
+
lines.push('='.repeat(title.length));
|
|
71
|
+
lines.push('');
|
|
72
|
+
|
|
73
|
+
// Pips
|
|
74
|
+
lines.push(`Pips (${manifest.pips.length})`);
|
|
75
|
+
if (manifest.pips.length === 0) {
|
|
76
|
+
lines.push(' (none)');
|
|
77
|
+
} else {
|
|
78
|
+
const widthName = Math.max(6, ...manifest.pips.map(p => p.name.length));
|
|
79
|
+
lines.push(` ${pad('NAME', widthName)} VERSION DEPENDENCIES`);
|
|
80
|
+
for (const p of manifest.pips) {
|
|
81
|
+
const version = p.version ?? '-';
|
|
82
|
+
const deps = p.dependencies.length > 0 ? p.dependencies.join(', ') : '-';
|
|
83
|
+
lines.push(` ${pad(p.name, widthName)} ${pad(version, 7)} ${deps}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
// Services
|
|
89
|
+
lines.push(`Services (${manifest.services.length})`);
|
|
90
|
+
if (manifest.services.length === 0) {
|
|
91
|
+
lines.push(' (none)');
|
|
92
|
+
} else {
|
|
93
|
+
const widthName = Math.max(4, ...manifest.services.map(s => s.name.length));
|
|
94
|
+
const widthKind = Math.max(4, ...manifest.services.map(s => s.kind.length));
|
|
95
|
+
lines.push(` ${pad('NAME', widthName)} ${pad('KIND', widthKind)} PLUGIN`);
|
|
96
|
+
for (const s of manifest.services) {
|
|
97
|
+
lines.push(` ${pad(s.name, widthName)} ${pad(s.kind, widthKind)} ${s.pip}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
|
|
102
|
+
// Routes
|
|
103
|
+
lines.push(`Routes (${manifest.routes.length})`);
|
|
104
|
+
if (manifest.routes.length === 0) {
|
|
105
|
+
lines.push(' (none)');
|
|
106
|
+
} else {
|
|
107
|
+
const widthId = Math.max(2, ...manifest.routes.map(r => r.id.length));
|
|
108
|
+
const widthMethod = Math.max(6, ...manifest.routes.map(r => (r.method ?? '-').length));
|
|
109
|
+
lines.push(` ${pad('ID', widthId)} ${pad('METHOD', widthMethod)} PATH/TRANSPORT PLUGIN`);
|
|
110
|
+
for (const r of manifest.routes) {
|
|
111
|
+
const method = r.method ?? '-';
|
|
112
|
+
const target = r.path ?? `(${r.transport})`;
|
|
113
|
+
lines.push(` ${pad(r.id, widthId)} ${pad(method, widthMethod)} ${pad(target, 14)} ${r.pip}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
|
|
118
|
+
// Dependencies
|
|
119
|
+
lines.push(`Dependencies (${manifest.dependencies.length})`);
|
|
120
|
+
if (manifest.dependencies.length === 0) {
|
|
121
|
+
lines.push(' (none)');
|
|
122
|
+
} else {
|
|
123
|
+
for (const d of manifest.dependencies) {
|
|
124
|
+
lines.push(` ${d.from} --[${d.kind}]--> ${d.to}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
|
|
129
|
+
// Lifecycle
|
|
130
|
+
lines.push(`Lifecycle hooks (${manifest.lifecycle.length})`);
|
|
131
|
+
if (manifest.lifecycle.length === 0) {
|
|
132
|
+
lines.push(' (none)');
|
|
133
|
+
} else {
|
|
134
|
+
const widthPip = Math.max(6, ...manifest.lifecycle.map(l => l.pip.length));
|
|
135
|
+
lines.push(` ${pad('PLUGIN', widthPip)} HOOKS`);
|
|
136
|
+
for (const l of manifest.lifecycle) {
|
|
137
|
+
const hooks = l.hooks.length > 0 ? l.hooks.join(', ') : '-';
|
|
138
|
+
lines.push(` ${pad(l.pip, widthPip)} ${hooks}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
lines.push('');
|
|
142
|
+
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render the explain output. Returns the envelope so callers can act on it
|
|
148
|
+
* (e.g. set the process exit code based on `status`).
|
|
149
|
+
*/
|
|
150
|
+
export function renderExplain(source: ExplainSource, opts: RenderOptions): DotCliEnvelope<DotAppManifest> {
|
|
151
|
+
const envelope = buildExplainEnvelope(source, opts);
|
|
152
|
+
const out = opts.out ?? defaultOut;
|
|
153
|
+
|
|
154
|
+
if (opts.json) {
|
|
155
|
+
out(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
156
|
+
} else {
|
|
157
|
+
out(`${renderTextManifest(source.manifest)}\n`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return envelope;
|
|
161
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the DOT kernel (v2).
|
|
3
|
+
*
|
|
4
|
+
* `defineApp(name)` returns a `DotAppBuilder` that accumulates pips via
|
|
5
|
+
* `.use(pip)`, then transitions through the 5-hook lifecycle:
|
|
6
|
+
*
|
|
7
|
+
* defineApp -> use* -> configure() -> boot() -> start() -> stop() -> dispose()
|
|
8
|
+
*
|
|
9
|
+
* `.use()` is compile-time guarded: a pip whose `needs` are not satisfied
|
|
10
|
+
* by services provided so far — or whose provides collide with existing
|
|
11
|
+
* wire keys — fails to typecheck at the call site ("Expected 2 arguments,
|
|
12
|
+
* but got 1", with the diagnostic embedded in the expected second
|
|
13
|
+
* argument's type). Declaration order IS boot order.
|
|
14
|
+
*
|
|
15
|
+
* Most callers don't need to call `configure()` explicitly — `boot()` runs it
|
|
16
|
+
* implicitly. `boot()` is also implicit when starting from `defined` via
|
|
17
|
+
* `start()`.
|
|
18
|
+
*
|
|
19
|
+
* See `./lifecycle.ts` for hook semantics, failure ordering, idempotency rules.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { DotDiagnosticsSnapshot } from './diagnostics.js';
|
|
23
|
+
import type { DotLifecycleObserver } from './lifecycle-observer.js';
|
|
24
|
+
import type { DotLifecycleState } from './lifecycle.js';
|
|
25
|
+
import type { DotAppManifest } from './manifest.js';
|
|
26
|
+
import type { AnyPip, EmptyShape, Pip, ServiceRecord } from './pip-contract.js';
|
|
27
|
+
import { DotAppImpl } from './kernel/app-instance.js';
|
|
28
|
+
import { renderTimeline } from './timeline.js';
|
|
29
|
+
|
|
30
|
+
/* ------------------------------------------------------------------ */
|
|
31
|
+
/* Compile-time wiring guard */
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
|
|
34
|
+
type MissingKeys<TAvail, TNeeds> = {
|
|
35
|
+
readonly [K in Exclude<keyof TNeeds, keyof TAvail>]: TNeeds[K];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type MismatchedKeys<TAvail, TNeeds> = {
|
|
39
|
+
readonly [K in keyof TAvail & keyof TNeeds as TAvail[K] extends TNeeds[K] ? never : K]: {
|
|
40
|
+
readonly provided: TAvail[K];
|
|
41
|
+
readonly needed: TNeeds[K];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type NeedsError<TAvail extends ServiceRecord, TNeeds extends ServiceRecord> = {
|
|
46
|
+
readonly 'DOT: pip needs services no earlier .use() provides': {
|
|
47
|
+
readonly missing: MissingKeys<TAvail, TNeeds>;
|
|
48
|
+
readonly mismatched: MismatchedKeys<TAvail, TNeeds>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type CollisionError<TAvail, TProvides> = {
|
|
53
|
+
readonly 'DOT: pip provides wire keys already provided (use rename())': keyof TAvail & keyof TProvides;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A pip whose `boot` always throws infers `TProvides = never`, which would
|
|
58
|
+
* poison both the collision check (`keyof never` matches everything) and
|
|
59
|
+
* the accumulated record (`TAvail & never` = `never`). Such a pip provides
|
|
60
|
+
* nothing — normalize to the empty shape.
|
|
61
|
+
*/
|
|
62
|
+
type NormalizeProvides<TP extends ServiceRecord> = [TP] extends [never] ? EmptyShape : TP;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rest-tuple guard. Satisfied → `[]` (call `.use(pip)` with one argument).
|
|
66
|
+
* Violated → a required second argument of an unconstructible error type,
|
|
67
|
+
* so the call site fails with the diagnostic embedded in the expected type.
|
|
68
|
+
*/
|
|
69
|
+
export type UseGuard<
|
|
70
|
+
TAvail extends ServiceRecord,
|
|
71
|
+
TNeeds extends ServiceRecord,
|
|
72
|
+
TProvides extends ServiceRecord,
|
|
73
|
+
> = [TAvail] extends [TNeeds]
|
|
74
|
+
? [keyof TAvail & keyof TProvides] extends [never]
|
|
75
|
+
? []
|
|
76
|
+
: [error: CollisionError<TAvail, TProvides>]
|
|
77
|
+
: [error: NeedsError<TAvail, TNeeds>];
|
|
78
|
+
|
|
79
|
+
/* ------------------------------------------------------------------ */
|
|
80
|
+
/* Public app surface */
|
|
81
|
+
/* ------------------------------------------------------------------ */
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Public DotApp surface. The internal `DotAppImpl` implements this; consumers
|
|
85
|
+
* see only these members.
|
|
86
|
+
*/
|
|
87
|
+
export type DotApp<TServices extends ServiceRecord> = {
|
|
88
|
+
/** App name (passed to `defineApp`). */
|
|
89
|
+
readonly name: string;
|
|
90
|
+
/** Current lifecycle state. */
|
|
91
|
+
readonly state: DotLifecycleState;
|
|
92
|
+
/**
|
|
93
|
+
* Services published by booted pips, keyed by wire key.
|
|
94
|
+
* Empty before `boot()` succeeds.
|
|
95
|
+
*/
|
|
96
|
+
readonly services: TServices;
|
|
97
|
+
/** Declarative manifest — describes the static shape of the app. */
|
|
98
|
+
readonly manifest: DotAppManifest;
|
|
99
|
+
/** Point-in-time diagnostics snapshot. Re-computed on every access. */
|
|
100
|
+
readonly diagnostics: DotDiagnosticsSnapshot;
|
|
101
|
+
/**
|
|
102
|
+
* Start active work. Boots first if app is `defined` or `configured`.
|
|
103
|
+
* Idempotent while `started`. Throws if app is `failed` or `disposed`.
|
|
104
|
+
*/
|
|
105
|
+
start(): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Stop active work. Keeps booted resources for later cleanup.
|
|
108
|
+
* Idempotent while not `started`.
|
|
109
|
+
*/
|
|
110
|
+
stop(): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Release booted resources. Runs stop() first if `started`.
|
|
113
|
+
* Idempotent while `disposed`.
|
|
114
|
+
*/
|
|
115
|
+
dispose(): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Register an in-process lifecycle observer. Returns an unsubscribe
|
|
118
|
+
* function. Observers added here see events from this point onward —
|
|
119
|
+
* pass observers through `defineApp(name, { observers })` to catch
|
|
120
|
+
* `configure`-phase events too.
|
|
121
|
+
*/
|
|
122
|
+
subscribe(observer: DotLifecycleObserver): () => void;
|
|
123
|
+
/**
|
|
124
|
+
* Render the recorded lifecycle as an ASCII waterfall. Reads from
|
|
125
|
+
* the current `diagnostics` snapshot — call after `boot()` /
|
|
126
|
+
* `dispose()` / a failure to see the full picture.
|
|
127
|
+
*/
|
|
128
|
+
timeline(): string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Intermediate type after `configure()` but before `boot()`.
|
|
133
|
+
* Exposes the manifest and diagnostics already, but no `services` yet.
|
|
134
|
+
*/
|
|
135
|
+
export type DotAppConfigured<TServices extends ServiceRecord> = {
|
|
136
|
+
readonly name: string;
|
|
137
|
+
readonly state: DotLifecycleState;
|
|
138
|
+
readonly manifest: DotAppManifest;
|
|
139
|
+
readonly diagnostics: DotDiagnosticsSnapshot;
|
|
140
|
+
/** Continue the lifecycle. */
|
|
141
|
+
boot(): Promise<DotApp<TServices>>;
|
|
142
|
+
start(): Promise<DotApp<TServices>>;
|
|
143
|
+
/** See {@link DotApp.subscribe}. */
|
|
144
|
+
subscribe(observer: DotLifecycleObserver): () => void;
|
|
145
|
+
/** See {@link DotApp.timeline}. */
|
|
146
|
+
timeline(): string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Builder produced by `defineApp(name)`.
|
|
151
|
+
*
|
|
152
|
+
* `.use(pip)` is type-tracking in both directions: the pip's `needs` must
|
|
153
|
+
* be satisfied by the services accumulated so far, and its `provides`
|
|
154
|
+
* merge into the accumulated record for the next `.use()`.
|
|
155
|
+
*/
|
|
156
|
+
export type DotAppBuilder<TAvail extends ServiceRecord> = {
|
|
157
|
+
/**
|
|
158
|
+
* Register a pip. Compile error when the pip's needs are unsatisfied
|
|
159
|
+
* or its provides collide with existing wire keys.
|
|
160
|
+
*/
|
|
161
|
+
use<TNeeds extends ServiceRecord, TProvides extends ServiceRecord>(
|
|
162
|
+
pip: Pip<TNeeds, TProvides>,
|
|
163
|
+
...guard: UseGuard<TAvail, TNeeds, NormalizeProvides<TProvides>>
|
|
164
|
+
): DotAppBuilder<TAvail & NormalizeProvides<TProvides>>;
|
|
165
|
+
/** Run the configure phase synchronously. Throws on configure failure. */
|
|
166
|
+
configure(): DotAppConfigured<TAvail>;
|
|
167
|
+
/** Run configure + boot. Throws on configure or boot failure. */
|
|
168
|
+
boot(): Promise<DotApp<TAvail>>;
|
|
169
|
+
/** Convenience: configure + boot + start. */
|
|
170
|
+
start(): Promise<DotApp<TAvail>>;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type BuilderState = {
|
|
174
|
+
appName: string;
|
|
175
|
+
appVersion?: string;
|
|
176
|
+
pips: AnyPip[];
|
|
177
|
+
config?: Readonly<Record<string, unknown>>;
|
|
178
|
+
observers?: readonly DotLifecycleObserver[];
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create a new DOT app builder.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* const app = await defineApp('my-app')
|
|
186
|
+
* .use(dbPip)
|
|
187
|
+
* .use(billingPip) // billing's needs must be satisfied by now
|
|
188
|
+
* .boot();
|
|
189
|
+
*
|
|
190
|
+
* await app.start();
|
|
191
|
+
* console.log(app.manifest);
|
|
192
|
+
* // ...
|
|
193
|
+
* await app.dispose();
|
|
194
|
+
*/
|
|
195
|
+
export function defineApp(
|
|
196
|
+
name: string,
|
|
197
|
+
options: {
|
|
198
|
+
version?: string;
|
|
199
|
+
config?: Readonly<Record<string, unknown>>;
|
|
200
|
+
/**
|
|
201
|
+
* In-process lifecycle observers, registered before the first phase
|
|
202
|
+
* fires. Required if you want to see `configure`-phase events — after
|
|
203
|
+
* configure runs, observers can be added post-hoc via
|
|
204
|
+
* `configured.subscribe(...)` or `app.subscribe(...)`.
|
|
205
|
+
*/
|
|
206
|
+
observers?: readonly DotLifecycleObserver[];
|
|
207
|
+
} = {},
|
|
208
|
+
): DotAppBuilder<EmptyShape> {
|
|
209
|
+
const state: BuilderState = {
|
|
210
|
+
appName: name,
|
|
211
|
+
appVersion: options.version,
|
|
212
|
+
pips: [],
|
|
213
|
+
config: options.config,
|
|
214
|
+
observers: options.observers,
|
|
215
|
+
};
|
|
216
|
+
return makeBuilder<EmptyShape>(state);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildImpl(state: BuilderState): DotAppImpl {
|
|
220
|
+
return new DotAppImpl({
|
|
221
|
+
appName: state.appName,
|
|
222
|
+
appVersion: state.appVersion,
|
|
223
|
+
pips: state.pips,
|
|
224
|
+
config: state.config,
|
|
225
|
+
observers: state.observers,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function makeBuilder<TAvail extends ServiceRecord>(state: BuilderState): DotAppBuilder<TAvail> {
|
|
230
|
+
// The `use` implementation is signature-erased (the guard exists purely
|
|
231
|
+
// at the type level); the single cast below is the same kernel boundary
|
|
232
|
+
// v1 crossed in its wrapApp helper.
|
|
233
|
+
const impl = {
|
|
234
|
+
use(pip: AnyPip, ..._guard: readonly unknown[]): DotAppBuilder<ServiceRecord> {
|
|
235
|
+
const nextState: BuilderState = {
|
|
236
|
+
...state,
|
|
237
|
+
pips: [...state.pips, pip],
|
|
238
|
+
};
|
|
239
|
+
return makeBuilder<ServiceRecord>(nextState);
|
|
240
|
+
},
|
|
241
|
+
configure(): DotAppConfigured<ServiceRecord> {
|
|
242
|
+
const appImpl = buildImpl(state);
|
|
243
|
+
appImpl.runConfigure();
|
|
244
|
+
return wrapConfigured<ServiceRecord>(appImpl);
|
|
245
|
+
},
|
|
246
|
+
async boot(): Promise<DotApp<ServiceRecord>> {
|
|
247
|
+
const appImpl = buildImpl(state);
|
|
248
|
+
await appImpl.boot();
|
|
249
|
+
return wrapApp<ServiceRecord>(appImpl);
|
|
250
|
+
},
|
|
251
|
+
async start(): Promise<DotApp<ServiceRecord>> {
|
|
252
|
+
const appImpl = buildImpl(state);
|
|
253
|
+
await appImpl.start();
|
|
254
|
+
return wrapApp<ServiceRecord>(appImpl);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
return impl as DotAppBuilder<TAvail>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function wrapApp<TServices extends ServiceRecord>(impl: DotAppImpl): DotApp<TServices> {
|
|
261
|
+
return {
|
|
262
|
+
get name() {
|
|
263
|
+
return impl.name;
|
|
264
|
+
},
|
|
265
|
+
get state() {
|
|
266
|
+
return impl.state;
|
|
267
|
+
},
|
|
268
|
+
get services() {
|
|
269
|
+
return impl.services as TServices;
|
|
270
|
+
},
|
|
271
|
+
get manifest() {
|
|
272
|
+
return impl.manifest;
|
|
273
|
+
},
|
|
274
|
+
get diagnostics() {
|
|
275
|
+
return impl.diagnostics;
|
|
276
|
+
},
|
|
277
|
+
start: () => impl.start(),
|
|
278
|
+
stop: () => impl.stop(),
|
|
279
|
+
dispose: () => impl.dispose(),
|
|
280
|
+
subscribe: observer => impl.subscribe(observer),
|
|
281
|
+
timeline: () => renderTimeline(impl.diagnostics),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function wrapConfigured<TServices extends ServiceRecord>(impl: DotAppImpl): DotAppConfigured<TServices> {
|
|
286
|
+
return {
|
|
287
|
+
get name() {
|
|
288
|
+
return impl.name;
|
|
289
|
+
},
|
|
290
|
+
get state() {
|
|
291
|
+
return impl.state;
|
|
292
|
+
},
|
|
293
|
+
get manifest() {
|
|
294
|
+
return impl.manifest;
|
|
295
|
+
},
|
|
296
|
+
get diagnostics() {
|
|
297
|
+
return impl.diagnostics;
|
|
298
|
+
},
|
|
299
|
+
async boot() {
|
|
300
|
+
await impl.boot();
|
|
301
|
+
return wrapApp<TServices>(impl);
|
|
302
|
+
},
|
|
303
|
+
async start() {
|
|
304
|
+
await impl.start();
|
|
305
|
+
return wrapApp<TServices>(impl);
|
|
306
|
+
},
|
|
307
|
+
subscribe: observer => impl.subscribe(observer),
|
|
308
|
+
timeline: () => renderTimeline(impl.diagnostics),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics types for the DOT kernel.
|
|
3
|
+
*
|
|
4
|
+
* Where the manifest describes the static shape of an app, a
|
|
5
|
+
* `DotDiagnosticsSnapshot` is a point-in-time observability record: the
|
|
6
|
+
* lifecycle state of the app and every pip, plus structured issues with
|
|
7
|
+
* remediation guidance.
|
|
8
|
+
*
|
|
9
|
+
* CONTRACT: `DotDiagnosticsSnapshot` always exposes the same five arrays
|
|
10
|
+
* (`pips`, `routes`, `services`, `lifecycle`, `issues`). Consumers must
|
|
11
|
+
* never see an omitted array — empty is empty, but never missing.
|
|
12
|
+
* This is the "5 arrays" contract referenced by the kernel spec.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DotLifecycleHook, DotLifecycleState } from './lifecycle.js';
|
|
16
|
+
|
|
17
|
+
/** Severity of a single diagnostic issue. */
|
|
18
|
+
export type DiagnosticSeverity = 'info' | 'warning' | 'error';
|
|
19
|
+
|
|
20
|
+
/** Status of a pip/route/service/lifecycle entry. */
|
|
21
|
+
export type DiagnosticStatus = 'ok' | 'degraded' | 'failed' | 'skipped' | 'missing';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Single point-in-time diagnostic snapshot of a DotApp.
|
|
25
|
+
*
|
|
26
|
+
* Five top-level arrays — pips, routes, services, lifecycle, issues —
|
|
27
|
+
* are guaranteed to be present even when empty.
|
|
28
|
+
*/
|
|
29
|
+
export type DotDiagnosticsSnapshot = {
|
|
30
|
+
/** ISO-8601 timestamp at which the snapshot was generated. */
|
|
31
|
+
generatedAt: string;
|
|
32
|
+
app: {
|
|
33
|
+
name: string;
|
|
34
|
+
state: DotLifecycleState;
|
|
35
|
+
};
|
|
36
|
+
pips: PipDiagnostic[];
|
|
37
|
+
routes: RouteDiagnostic[];
|
|
38
|
+
services: ServiceDiagnostic[];
|
|
39
|
+
lifecycle: LifecycleDiagnostic[];
|
|
40
|
+
issues: DiagnosticIssue[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type PipDiagnostic = {
|
|
44
|
+
pip: string;
|
|
45
|
+
status: Extract<DiagnosticStatus, 'ok' | 'degraded' | 'failed' | 'skipped'>;
|
|
46
|
+
issues: DiagnosticIssue[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type RouteDiagnostic = {
|
|
50
|
+
id: string;
|
|
51
|
+
pip: string;
|
|
52
|
+
status: Extract<DiagnosticStatus, 'ok' | 'degraded' | 'failed'>;
|
|
53
|
+
issues: DiagnosticIssue[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type ServiceDiagnostic = {
|
|
57
|
+
service: string;
|
|
58
|
+
pip: string;
|
|
59
|
+
status: Extract<DiagnosticStatus, 'ok' | 'degraded' | 'failed' | 'missing'>;
|
|
60
|
+
issues: DiagnosticIssue[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type LifecycleDiagnostic = {
|
|
64
|
+
pip: string;
|
|
65
|
+
hook: DotLifecycleHook;
|
|
66
|
+
state: DotLifecycleState;
|
|
67
|
+
/** 0-based topological order of this pip within the hook pass. */
|
|
68
|
+
order: number;
|
|
69
|
+
/** Wall-clock duration of the hook call, in milliseconds. */
|
|
70
|
+
durationMs?: number;
|
|
71
|
+
issues: DiagnosticIssue[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Single structured issue surfaced by the kernel or by a pip.
|
|
76
|
+
*
|
|
77
|
+
* `code`, `message`, `remediation` and `docsUrl` are required so that issues
|
|
78
|
+
* are always actionable — never just "something went wrong".
|
|
79
|
+
*/
|
|
80
|
+
export type DiagnosticIssue = {
|
|
81
|
+
/** Stable machine-readable code, e.g. `DOT_LIFECYCLE_E003`. */
|
|
82
|
+
code: string;
|
|
83
|
+
severity: DiagnosticSeverity;
|
|
84
|
+
pip?: string;
|
|
85
|
+
message: string;
|
|
86
|
+
/** Human-readable remediation: what the developer should do. */
|
|
87
|
+
remediation: string;
|
|
88
|
+
/** Link to the relevant docs page for this issue. */
|
|
89
|
+
docsUrl: string;
|
|
90
|
+
metadata?: Record<string, unknown>;
|
|
91
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @arki/dot — TypeScript-first application composition framework
|
|
3
|
+
*
|
|
4
|
+
* Public surface:
|
|
5
|
+
* - `defineApp(name)` — the entry point for composing applications.
|
|
6
|
+
* - `pip(config)` — author lifecycle-aware pips with typed needs/provides.
|
|
7
|
+
* - `service<T>()` / `token<T>()(key)` — service witnesses for DI wiring.
|
|
8
|
+
* - `rename(pip, map)` — mount-time multi-instance primitive.
|
|
9
|
+
* - Lifecycle / manifest / diagnostics types.
|
|
10
|
+
* - `testApp` / `bootTestApp` — test harnesses for pip authors.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// #region Kernel — public surface
|
|
14
|
+
export { defineApp } from './define-app.js';
|
|
15
|
+
export type { DotApp, DotAppBuilder, DotAppConfigured, UseGuard } from './define-app.js';
|
|
16
|
+
|
|
17
|
+
export { isLazy, lazy, lazyOf, pip, provide, rename, service, token, DotPipError } from './pip-contract.js';
|
|
18
|
+
export type {
|
|
19
|
+
AnyPip,
|
|
20
|
+
CtxOf,
|
|
21
|
+
DotConfigureContext,
|
|
22
|
+
EmptyShape,
|
|
23
|
+
KernelCtx,
|
|
24
|
+
Lazy,
|
|
25
|
+
LazyService,
|
|
26
|
+
NeedsShape,
|
|
27
|
+
Pip,
|
|
28
|
+
PipNeeds,
|
|
29
|
+
PipProvides,
|
|
30
|
+
RenamedProvides,
|
|
31
|
+
Service,
|
|
32
|
+
ServiceRecord,
|
|
33
|
+
Token,
|
|
34
|
+
WireNeeds,
|
|
35
|
+
} from './pip-contract.js';
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
DotLifecycleError,
|
|
39
|
+
DotLifecycleErrorCode,
|
|
40
|
+
DOT_LIFECYCLE_HOOKS,
|
|
41
|
+
} from './lifecycle.js';
|
|
42
|
+
export type {
|
|
43
|
+
DotLifecycleHook,
|
|
44
|
+
DotLifecycleState,
|
|
45
|
+
DotLifecyclePipFailure,
|
|
46
|
+
DotLifecycleErrorCodeValue,
|
|
47
|
+
} from './lifecycle.js';
|
|
48
|
+
|
|
49
|
+
export type {
|
|
50
|
+
DotAppManifest,
|
|
51
|
+
PipManifest,
|
|
52
|
+
RouteManifest,
|
|
53
|
+
ServiceManifest,
|
|
54
|
+
LifecycleManifest,
|
|
55
|
+
DependencyEdge,
|
|
56
|
+
DependencyEdgeKind,
|
|
57
|
+
ServiceKind,
|
|
58
|
+
RouteTransport,
|
|
59
|
+
} from './manifest.js';
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
DotDiagnosticsSnapshot,
|
|
63
|
+
PipDiagnostic,
|
|
64
|
+
RouteDiagnostic,
|
|
65
|
+
ServiceDiagnostic,
|
|
66
|
+
LifecycleDiagnostic,
|
|
67
|
+
DiagnosticIssue,
|
|
68
|
+
DiagnosticSeverity,
|
|
69
|
+
DiagnosticStatus,
|
|
70
|
+
} from './diagnostics.js';
|
|
71
|
+
|
|
72
|
+
export type {
|
|
73
|
+
DotLifecycleEvent,
|
|
74
|
+
DotLifecycleEventStatus,
|
|
75
|
+
DotLifecycleObserver,
|
|
76
|
+
DotPhaseLifecycleEvent,
|
|
77
|
+
DotPipHookLifecycleEvent,
|
|
78
|
+
} from './lifecycle-observer.js';
|
|
79
|
+
|
|
80
|
+
export { renderTimeline } from './timeline.js';
|
|
81
|
+
export type { RenderTimelineOptions } from './timeline.js';
|
|
82
|
+
|
|
83
|
+
export { testApp, bootTestApp } from './test-harness.js';
|
|
84
|
+
export type { TestAppOptions } from './test-harness.js';
|
|
85
|
+
|
|
86
|
+
// Task 9b: CLI envelope type is exported so adapter packages can produce the
|
|
87
|
+
// same shape from related tooling (release-tooling, pip scaffolds, etc.).
|
|
88
|
+
export type { DotCliEnvelope, DotCliEnvelopeStatus } from './cli/render-explain.js';
|
|
89
|
+
// #endregion
|