@arki/dot 0.2.0 → 0.4.0
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/README.md +86 -86
- package/dist/cli/discover.js +2 -2
- package/dist/cli/discover.js.map +1 -1
- package/dist/cli/error-codes.d.ts +8 -0
- package/dist/cli/error-codes.d.ts.map +1 -1
- package/dist/cli/error-codes.js +12 -0
- package/dist/cli/error-codes.js.map +1 -1
- package/dist/cli/index.d.ts +11 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +66 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/new.d.ts +1 -1
- package/dist/cli/new.js +1 -1
- package/dist/cli/render-doctor.d.ts +1 -1
- package/dist/cli/render-doctor.js +23 -23
- package/dist/cli/render-doctor.js.map +1 -1
- package/dist/cli/render-explain.d.ts.map +1 -1
- package/dist/cli/render-explain.js +33 -19
- package/dist/cli/render-explain.js.map +1 -1
- package/dist/cli/render-graph.d.ts +3 -3
- package/dist/cli/render-graph.js +12 -12
- package/dist/cli/render-graph.js.map +1 -1
- package/dist/cli/render-openapi.d.ts +38 -0
- package/dist/cli/render-openapi.d.ts.map +1 -0
- package/dist/cli/render-openapi.js +131 -0
- package/dist/cli/render-openapi.js.map +1 -0
- package/dist/cli/render-projection.d.ts +29 -0
- package/dist/cli/render-projection.d.ts.map +1 -0
- package/dist/cli/render-projection.js +202 -0
- package/dist/cli/render-projection.js.map +1 -0
- package/dist/define-app.d.ts +49 -17
- package/dist/define-app.d.ts.map +1 -1
- package/dist/define-app.js +16 -9
- package/dist/define-app.js.map +1 -1
- package/dist/diagnostics.d.ts +19 -15
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +2 -2
- package/dist/index.d.ts +15 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/init-pips.d.ts +45 -0
- package/dist/init-pips.d.ts.map +1 -0
- package/dist/init-pips.js +49 -0
- package/dist/init-pips.js.map +1 -0
- package/dist/init-plugins.d.ts +45 -0
- package/dist/init-plugins.d.ts.map +1 -0
- package/dist/init-plugins.js +49 -0
- package/dist/init-plugins.js.map +1 -0
- package/dist/kernel/app-instance.d.ts +5 -5
- package/dist/kernel/app-instance.d.ts.map +1 -1
- package/dist/kernel/app-instance.js +375 -244
- package/dist/kernel/app-instance.js.map +1 -1
- package/dist/kernel/otel.d.ts +19 -19
- package/dist/kernel/otel.d.ts.map +1 -1
- package/dist/kernel/otel.js +22 -22
- package/dist/kernel/otel.js.map +1 -1
- package/dist/lifecycle-observer.d.ts +10 -10
- package/dist/lifecycle-observer.d.ts.map +1 -1
- package/dist/lifecycle-observer.js +1 -1
- package/dist/lifecycle.d.ts +18 -14
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +13 -9
- package/dist/lifecycle.js.map +1 -1
- package/dist/manifest.d.ts +70 -29
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +74 -5
- package/dist/manifest.js.map +1 -1
- package/dist/pip-contract.d.ts +40 -10
- package/dist/pip-contract.d.ts.map +1 -1
- package/dist/pip-contract.js +1 -25
- package/dist/pip-contract.js.map +1 -1
- package/dist/pip.d.ts +7 -4
- package/dist/pip.d.ts.map +1 -1
- package/dist/pip.js +2 -0
- package/dist/pip.js.map +1 -1
- package/dist/plugin-contract.d.ts +335 -143
- package/dist/plugin-contract.d.ts.map +1 -1
- package/dist/plugin-contract.js +201 -36
- package/dist/plugin-contract.js.map +1 -1
- package/dist/plugin.d.ts +18 -15
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +11 -11
- package/dist/plugin.js.map +1 -1
- package/dist/test-harness.d.ts +31 -31
- package/dist/test-harness.d.ts.map +1 -1
- package/dist/test-harness.js +29 -28
- package/dist/test-harness.js.map +1 -1
- package/dist/timeline.d.ts +2 -2
- package/dist/timeline.js +5 -5
- package/dist/timeline.js.map +1 -1
- package/package.json +9 -9
- package/src/cli/discover.ts +2 -2
- package/src/cli/error-codes.ts +12 -0
- package/src/cli/index.ts +93 -15
- package/src/cli/new.ts +1 -1
- package/src/cli/render-doctor.ts +21 -21
- package/src/cli/render-explain.ts +37 -19
- package/src/cli/render-graph.ts +12 -12
- package/src/cli/render-projection.ts +239 -0
- package/src/define-app.ts +85 -22
- package/src/diagnostics.ts +19 -15
- package/src/index.ts +38 -20
- package/src/init-plugins.ts +142 -0
- package/src/kernel/app-instance.ts +428 -282
- package/src/kernel/otel.ts +27 -27
- package/src/lifecycle-observer.ts +10 -10
- package/src/lifecycle.ts +19 -15
- package/src/manifest.ts +128 -31
- package/src/{pip-contract.ts → plugin-contract.ts} +83 -44
- package/src/{pip.ts → plugin.ts} +38 -18
- package/src/test-harness.ts +52 -51
- package/src/timeline.ts +6 -6
- package/templates/app-minimal/AGENTS.md.tmpl +9 -9
- package/templates/app-minimal/README.md.tmpl +1 -1
- package/templates/app-minimal/tests/boot.test.ts.tmpl +1 -1
package/src/kernel/otel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenTelemetry instrumentation for the DOT kernel.
|
|
3
3
|
*
|
|
4
|
-
* DOT is OTel-first: every lifecycle phase and every per-
|
|
4
|
+
* DOT is OTel-first: every lifecycle phase and every per-plugin hook
|
|
5
5
|
* automatically emits a span and a duration histogram. When no SDK is
|
|
6
6
|
* registered, the OTel API returns no-op implementations and the kernel
|
|
7
7
|
* pays zero allocation per call — the discipline that makes "OTel-first"
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* - Phase span: `dot.app.<phase>` — one root span per `configure()` /
|
|
16
16
|
* `boot()` / `start()` / `stop()` / `dispose()` call. Children
|
|
17
17
|
* auto-link via async-context.
|
|
18
|
-
* - Hook span: `dot.
|
|
19
|
-
* execution. The
|
|
20
|
-
* name, so backends can aggregate by hook across
|
|
21
|
-
* - Attributes (`dot.app.name`, `dot.
|
|
22
|
-
* `dot.hook`, `dot.
|
|
18
|
+
* - Hook span: `dot.plugin.<hook>` — one child span per plugin's hook
|
|
19
|
+
* execution. The plugin name lives on `dot.plugin.name`, not in the span
|
|
20
|
+
* name, so backends can aggregate by hook across plugins.
|
|
21
|
+
* - Attributes (`dot.app.name`, `dot.plugin.name`, `dot.plugin.version`,
|
|
22
|
+
* `dot.hook`, `dot.plugin.order`) follow the OTel convention of
|
|
23
23
|
* namespacing under the library's prefix.
|
|
24
24
|
*
|
|
25
25
|
* @see packages/dot/docs/principles.md — principle 3 (deterministic) +
|
|
@@ -41,16 +41,16 @@ export const tracer = trace.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERS
|
|
|
41
41
|
export const meter = metrics.getMeter(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Histogram recording per-
|
|
44
|
+
* Histogram recording per-plugin per-hook execution duration in milliseconds.
|
|
45
45
|
*
|
|
46
46
|
* Attributes:
|
|
47
47
|
* - `dot.app.name` — app name
|
|
48
|
-
* - `dot.
|
|
48
|
+
* - `dot.plugin.name` — plugin name
|
|
49
49
|
* - `dot.hook` — which lifecycle hook (`configure` | `boot` | ...)
|
|
50
50
|
* - `dot.status` — `ok` | `failed`
|
|
51
51
|
*/
|
|
52
|
-
export const hookDurationHistogram = meter.createHistogram('dot.
|
|
53
|
-
description: 'Wall-clock duration of a single
|
|
52
|
+
export const hookDurationHistogram = meter.createHistogram('dot.plugin.hook.duration', {
|
|
53
|
+
description: 'Wall-clock duration of a single plugin lifecycle hook execution.',
|
|
54
54
|
unit: 'ms',
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -95,44 +95,44 @@ function stringifyForSpan(value: unknown): string {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* Wrap a single
|
|
98
|
+
* Wrap a single plugin-hook execution in a child span and record its
|
|
99
99
|
* duration in the histogram. The span auto-links to the active phase
|
|
100
100
|
* span (set by {@link withPhaseSpan}) so traces show a clean parent-child
|
|
101
101
|
* hierarchy:
|
|
102
102
|
*
|
|
103
103
|
* dot.app.boot
|
|
104
|
-
* ├── dot.
|
|
105
|
-
* ├── dot.
|
|
106
|
-
* └── dot.
|
|
104
|
+
* ├── dot.plugin.boot (dot.plugin.name=env, dot.hook=boot)
|
|
105
|
+
* ├── dot.plugin.boot (dot.plugin.name=db, dot.hook=boot)
|
|
106
|
+
* └── dot.plugin.boot (dot.plugin.name=kv, dot.hook=boot)
|
|
107
107
|
*
|
|
108
108
|
* Re-throws on failure after marking the span ERROR so the caller's
|
|
109
109
|
* existing error-handling path stays intact.
|
|
110
110
|
*/
|
|
111
|
-
export function
|
|
111
|
+
export function withPluginHookSpan<R>(
|
|
112
112
|
opts: {
|
|
113
113
|
readonly appName: string;
|
|
114
|
-
readonly
|
|
115
|
-
readonly
|
|
114
|
+
readonly pluginName: string;
|
|
115
|
+
readonly pluginVersion?: string;
|
|
116
116
|
readonly hook: DotLifecycleHook;
|
|
117
117
|
readonly order: number;
|
|
118
118
|
/**
|
|
119
|
-
* Optional
|
|
119
|
+
* Optional plugin-scoped logger. When present, the helper threads the
|
|
120
120
|
* span's `traceId` + `spanId` onto the logger via `setTraceContext`
|
|
121
121
|
* before invoking the body — every log line emitted from within the
|
|
122
|
-
*
|
|
122
|
+
* plugin's hook is automatically correlated with this span in the
|
|
123
123
|
* `traceId`/`spanId` fields of `LogRecord`.
|
|
124
124
|
*/
|
|
125
125
|
readonly logger?: Logger;
|
|
126
126
|
},
|
|
127
127
|
fn: (span: Span) => R,
|
|
128
128
|
): R {
|
|
129
|
-
const span = tracer.startSpan(`dot.
|
|
129
|
+
const span = tracer.startSpan(`dot.plugin.${opts.hook}`, {
|
|
130
130
|
attributes: attrs({
|
|
131
131
|
'dot.app.name': opts.appName,
|
|
132
|
-
'dot.
|
|
133
|
-
'dot.
|
|
132
|
+
'dot.plugin.name': opts.pluginName,
|
|
133
|
+
'dot.plugin.version': opts.pluginVersion,
|
|
134
134
|
'dot.hook': opts.hook,
|
|
135
|
-
'dot.
|
|
135
|
+
'dot.plugin.order': opts.order,
|
|
136
136
|
}),
|
|
137
137
|
});
|
|
138
138
|
if (opts.logger) {
|
|
@@ -145,7 +145,7 @@ export function withPipHookSpan<R>(
|
|
|
145
145
|
const durationMs = performance.now() - started;
|
|
146
146
|
hookDurationHistogram.record(durationMs, {
|
|
147
147
|
'dot.app.name': opts.appName,
|
|
148
|
-
'dot.
|
|
148
|
+
'dot.plugin.name': opts.pluginName,
|
|
149
149
|
'dot.hook': opts.hook,
|
|
150
150
|
'dot.status': status,
|
|
151
151
|
});
|
|
@@ -183,7 +183,7 @@ export function withPipHookSpan<R>(
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* Wrap an entire app-level phase (`configure`/`boot`/`start`/`stop`/`dispose`)
|
|
186
|
-
* in an active span. Children created by {@link
|
|
186
|
+
* in an active span. Children created by {@link withPluginHookSpan} inherit
|
|
187
187
|
* this span as their parent via async-context.
|
|
188
188
|
*
|
|
189
189
|
* The callback's return value (sync or Promise) is propagated unchanged
|
|
@@ -194,7 +194,7 @@ export function withPhaseSpan<R>(
|
|
|
194
194
|
readonly appName: string;
|
|
195
195
|
readonly appVersion?: string;
|
|
196
196
|
readonly phase: DotLifecycleHook;
|
|
197
|
-
readonly
|
|
197
|
+
readonly pluginCount: number;
|
|
198
198
|
/**
|
|
199
199
|
* Optional phase-scoped logger. When present, the helper threads
|
|
200
200
|
* the span's `traceId` + `spanId` onto the logger before invoking
|
|
@@ -213,7 +213,7 @@ export function withPhaseSpan<R>(
|
|
|
213
213
|
'dot.app.name': opts.appName,
|
|
214
214
|
'dot.app.version': opts.appVersion,
|
|
215
215
|
'dot.app.phase': opts.phase,
|
|
216
|
-
'dot.app.
|
|
216
|
+
'dot.app.plugin.count': opts.pluginCount,
|
|
217
217
|
}),
|
|
218
218
|
},
|
|
219
219
|
(span): R => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Lifecycle observer surface for the DOT kernel.
|
|
3
3
|
*
|
|
4
4
|
* `DotLifecycleObserver` is the in-process companion to the OTel signals
|
|
5
|
-
* emitted by {@link withPhaseSpan} / {@link
|
|
5
|
+
* emitted by {@link withPhaseSpan} / {@link withPluginHookSpan}. Where OTel
|
|
6
6
|
* is the contract for cross-process tracing (consumers register an SDK
|
|
7
7
|
* and ship spans to a backend), the observer is the contract for *local*
|
|
8
8
|
* programmatic inspection — used by tests, CLI tooling, ASCII waterfalls,
|
|
@@ -24,7 +24,7 @@ import type { DotLifecycleHook } from './lifecycle.js';
|
|
|
24
24
|
* observer. Discriminated by `kind`:
|
|
25
25
|
*
|
|
26
26
|
* - `'phase'` — boundary of a top-level lifecycle phase
|
|
27
|
-
* - `'
|
|
27
|
+
* - `'plugin-hook'` — boundary of a single plugin's hook execution
|
|
28
28
|
*
|
|
29
29
|
* Status discriminates the boundary itself:
|
|
30
30
|
*
|
|
@@ -35,7 +35,7 @@ import type { DotLifecycleHook } from './lifecycle.js';
|
|
|
35
35
|
* `starting` events never carry `durationMs` / `error`. `completed` events
|
|
36
36
|
* carry `durationMs`. `failed` events carry both `durationMs` and `error`.
|
|
37
37
|
*/
|
|
38
|
-
export type DotLifecycleEvent = DotPhaseLifecycleEvent |
|
|
38
|
+
export type DotLifecycleEvent = DotPhaseLifecycleEvent | DotPluginHookLifecycleEvent;
|
|
39
39
|
|
|
40
40
|
/** Possible event statuses. See {@link DotLifecycleEvent}. */
|
|
41
41
|
export type DotLifecycleEventStatus = 'starting' | 'completed' | 'failed';
|
|
@@ -56,14 +56,14 @@ export type DotPhaseLifecycleEvent = {
|
|
|
56
56
|
readonly timestamp: number;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
/** Boundary of a single
|
|
60
|
-
export type
|
|
61
|
-
readonly kind: '
|
|
59
|
+
/** Boundary of a single plugin's hook execution. */
|
|
60
|
+
export type DotPluginHookLifecycleEvent = {
|
|
61
|
+
readonly kind: 'plugin-hook';
|
|
62
62
|
/** The phase the hook belongs to. */
|
|
63
63
|
readonly phase: DotLifecycleHook;
|
|
64
|
-
/** The
|
|
65
|
-
readonly
|
|
66
|
-
/** Topological order of the
|
|
64
|
+
/** The plugin whose hook is being executed. */
|
|
65
|
+
readonly plugin: string;
|
|
66
|
+
/** Topological order of the plugin within the phase (0-based). */
|
|
67
67
|
readonly order: number;
|
|
68
68
|
readonly status: DotLifecycleEventStatus;
|
|
69
69
|
readonly appName: string;
|
|
@@ -91,7 +91,7 @@ export type DotPipHookLifecycleEvent = {
|
|
|
91
91
|
* const app = await defineApp('my-app', {
|
|
92
92
|
* observers: [(event) => events.push(event)],
|
|
93
93
|
* })
|
|
94
|
-
* .use(
|
|
94
|
+
* .use(myPlugin)
|
|
95
95
|
* .boot();
|
|
96
96
|
*
|
|
97
97
|
* // events now contains the full configure + boot event stream.
|
package/src/lifecycle.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type DotLifecycleHook = 'configure' | 'boot' | 'start' | 'stop' | 'dispos
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* The complete set of lifecycle hooks in execution order.
|
|
23
|
-
* `stop` and `dispose` run in reverse declaration order across
|
|
23
|
+
* `stop` and `dispose` run in reverse declaration order across plugins, but the
|
|
24
24
|
* sequence of hooks themselves is always `configure -> boot -> start -> stop -> dispose`.
|
|
25
25
|
*/
|
|
26
26
|
export const DOT_LIFECYCLE_HOOKS: readonly DotLifecycleHook[] = [
|
|
@@ -68,16 +68,20 @@ export const DotLifecycleErrorCode = {
|
|
|
68
68
|
// v2 wiring model — declaration order is boot order, so cycles and
|
|
69
69
|
// name-based dependency declarations no longer exist. Codes are never
|
|
70
70
|
// reused for new meanings.
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
/** A
|
|
71
|
+
/** Plugin registered twice. */
|
|
72
|
+
DuplicatePlugin: 'DOT_LIFECYCLE_E011',
|
|
73
|
+
/** A plugin's `needs` entry has no provider among earlier-booted plugins. */
|
|
74
74
|
UnsatisfiedNeed: 'DOT_LIFECYCLE_E012',
|
|
75
|
-
/** A
|
|
75
|
+
/** A plugin published a wire key that an earlier plugin already provides. */
|
|
76
76
|
ServiceCollision: 'DOT_LIFECYCLE_E013',
|
|
77
77
|
/** A needs alias, publish key, or rename target uses the reserved `$` prefix. */
|
|
78
78
|
ReservedServiceKey: 'DOT_LIFECYCLE_E014',
|
|
79
79
|
/** A lifecycle hook exceeded the app's `hookTimeoutMs` watchdog. */
|
|
80
80
|
HookTimeout: 'DOT_LIFECYCLE_E015',
|
|
81
|
+
/** Two actions in the same binding use the same id. */
|
|
82
|
+
DuplicateAction: 'DOT_LIFECYCLE_E016',
|
|
83
|
+
/** Action metadata is not JSON-round-trip safe. */
|
|
84
|
+
ActionMetaNotJson: 'DOT_LIFECYCLE_E017',
|
|
81
85
|
} as const;
|
|
82
86
|
|
|
83
87
|
export type DotLifecycleErrorCodeValue = (typeof DotLifecycleErrorCode)[keyof typeof DotLifecycleErrorCode];
|
|
@@ -88,38 +92,38 @@ export type DotLifecycleErrorCodeValue = (typeof DotLifecycleErrorCode)[keyof ty
|
|
|
88
92
|
* Carries:
|
|
89
93
|
* - `code` — stable machine-readable error code (see {@link DotLifecycleErrorCode}).
|
|
90
94
|
* - `phase` — which hook (or pseudo-hook) failed.
|
|
91
|
-
* - `
|
|
95
|
+
* - `plugin` — which plugin name, when applicable.
|
|
92
96
|
* - `cause` — original error if wrapped from a hook throw.
|
|
93
|
-
* - `failures` — for aggregate errors (stop/dispose), the per-
|
|
97
|
+
* - `failures` — for aggregate errors (stop/dispose), the per-plugin failures.
|
|
94
98
|
*/
|
|
95
99
|
export class DotLifecycleError extends Error {
|
|
96
100
|
readonly code: DotLifecycleErrorCodeValue;
|
|
97
101
|
readonly phase: DotLifecycleHook;
|
|
98
|
-
readonly
|
|
102
|
+
readonly plugin?: string;
|
|
99
103
|
override readonly cause?: unknown;
|
|
100
|
-
readonly failures?: readonly
|
|
104
|
+
readonly failures?: readonly DotLifecyclePluginFailure[];
|
|
101
105
|
|
|
102
106
|
constructor(args: {
|
|
103
107
|
code: DotLifecycleErrorCodeValue;
|
|
104
108
|
phase: DotLifecycleHook;
|
|
105
109
|
message: string;
|
|
106
|
-
|
|
110
|
+
plugin?: string;
|
|
107
111
|
cause?: unknown;
|
|
108
|
-
failures?: readonly
|
|
112
|
+
failures?: readonly DotLifecyclePluginFailure[];
|
|
109
113
|
}) {
|
|
110
114
|
super(args.message);
|
|
111
115
|
this.name = 'DotLifecycleError';
|
|
112
116
|
this.code = args.code;
|
|
113
117
|
this.phase = args.phase;
|
|
114
|
-
this.
|
|
118
|
+
this.plugin = args.plugin;
|
|
115
119
|
this.cause = args.cause;
|
|
116
120
|
this.failures = args.failures;
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
/** Single
|
|
121
|
-
export type
|
|
122
|
-
|
|
124
|
+
/** Single plugin failure inside an aggregate lifecycle error (stop/dispose). */
|
|
125
|
+
export type DotLifecyclePluginFailure = {
|
|
126
|
+
plugin: string;
|
|
123
127
|
phase: DotLifecycleHook;
|
|
124
128
|
error: unknown;
|
|
125
129
|
};
|
package/src/manifest.ts
CHANGED
|
@@ -2,24 +2,96 @@
|
|
|
2
2
|
* Manifest types for the DOT kernel.
|
|
3
3
|
*
|
|
4
4
|
* A `DotAppManifest` is the static, declarative description of an app: which
|
|
5
|
-
*
|
|
5
|
+
* plugins are registered, what actions/services/projections they contribute, and how they
|
|
6
6
|
* depend on each other. It is built up during the `configure` phase from
|
|
7
7
|
* registration calls and finalised once `configure` completes.
|
|
8
8
|
*
|
|
9
|
-
* CONTRACT: `DotAppManifest` always exposes the same
|
|
10
|
-
* (`
|
|
11
|
-
* MUST NOT see an omitted array — empty is empty,
|
|
12
|
-
*
|
|
9
|
+
* CONTRACT: `DotAppManifest` always exposes the same six top-level arrays
|
|
10
|
+
* (`plugins`, `actions`, `services`, `lifecycle`, `dependencies`,
|
|
11
|
+
* `projections`). Consumers MUST NOT see an omitted array — empty is empty,
|
|
12
|
+
* but never missing.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { DotLifecycleHook } from './lifecycle.js';
|
|
16
16
|
|
|
17
|
+
/** JSON-serializable value — the manifest's currency. */
|
|
18
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
19
|
+
|
|
20
|
+
/** JSON-serializable object used for adapter-owned manifest metadata. */
|
|
21
|
+
export type JsonObject = { [key: string]: JsonValue };
|
|
22
|
+
|
|
23
|
+
function jsonRoundTrip(value: unknown): unknown {
|
|
24
|
+
let serialized: string;
|
|
25
|
+
try {
|
|
26
|
+
serialized = JSON.stringify(value);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new TypeError('Value must be JSON-serializable manifest data.', { cause: error });
|
|
29
|
+
}
|
|
30
|
+
if (serialized === undefined) {
|
|
31
|
+
throw new TypeError('Value must be a JSON-serializable object.');
|
|
32
|
+
}
|
|
33
|
+
return JSON.parse(serialized) as unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isJsonPrimitive(value: unknown): value is string | number | boolean | null {
|
|
37
|
+
return value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
42
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
43
|
+
return prototype === Object.prototype || prototype === null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
47
|
+
if (isJsonPrimitive(value)) return typeof value !== 'number' || Number.isFinite(value);
|
|
48
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
49
|
+
if (!isPlainObject(value)) return false;
|
|
50
|
+
return Object.values(value).every(isJsonValue);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
54
|
+
return isPlainObject(value) && Object.values(value).every(isJsonValue);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function jsonDeepEqual(left: unknown, right: unknown): boolean {
|
|
58
|
+
if (isJsonPrimitive(left) || isJsonPrimitive(right)) return Object.is(left, right);
|
|
59
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
60
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
61
|
+
return left.every((value, index) => jsonDeepEqual(value, right[index]));
|
|
62
|
+
}
|
|
63
|
+
if (!isPlainObject(left) || !isPlainObject(right)) return false;
|
|
64
|
+
const leftEntries = Object.entries(left);
|
|
65
|
+
const rightEntries = Object.entries(right);
|
|
66
|
+
if (leftEntries.length !== rightEntries.length) return false;
|
|
67
|
+
for (const [key, value] of leftEntries) {
|
|
68
|
+
if (!Object.hasOwn(right, key) || !jsonDeepEqual(value, right[key])) return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate and narrow unknown adapter data into manifest JSON metadata.
|
|
75
|
+
*
|
|
76
|
+
* The check intentionally compares the original value against a
|
|
77
|
+
* JSON.stringify/parse round trip. Dates, functions, undefined fields, NaN,
|
|
78
|
+
* Infinity, class instances, and cycles fail instead of being silently
|
|
79
|
+
* coerced in `dot explain --json`.
|
|
80
|
+
*/
|
|
81
|
+
export function toJsonObject(value: unknown): JsonObject {
|
|
82
|
+
const parsed = jsonRoundTrip(value);
|
|
83
|
+
if (!isJsonObject(parsed) || !jsonDeepEqual(value, parsed)) {
|
|
84
|
+
throw new TypeError('Value must be a JSON-serializable object without lossy coercions.');
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
|
|
17
89
|
/**
|
|
18
|
-
* Kind of service that a
|
|
90
|
+
* Kind of service that a plugin can publish.
|
|
19
91
|
*
|
|
20
92
|
* Well-known kinds map 1:1 to the canonical `@arki/*` adapters. `custom`
|
|
21
|
-
* is the escape hatch for
|
|
22
|
-
*
|
|
93
|
+
* is the escape hatch for plugin-defined service kinds — use it whenever a
|
|
94
|
+
* plugin publishes something that does not fit a reserved shape.
|
|
23
95
|
*/
|
|
24
96
|
export type ServiceKind =
|
|
25
97
|
| 'env'
|
|
@@ -33,60 +105,85 @@ export type ServiceKind =
|
|
|
33
105
|
| 'message-bus'
|
|
34
106
|
| 'custom';
|
|
35
107
|
|
|
36
|
-
/**
|
|
37
|
-
export type RouteTransport = 'http' | 'orpc' | 'trpc' | 'rpc' | 'custom';
|
|
38
|
-
|
|
39
|
-
/** Kind of dependency edge between two pips. */
|
|
108
|
+
/** Kind of dependency edge between two plugins. */
|
|
40
109
|
export type DependencyEdgeKind = 'requires' | 'provides' | 'uses';
|
|
41
110
|
|
|
111
|
+
/** Direction of a boundary interaction, viewed from the app. */
|
|
112
|
+
export type ActionDirection = 'in' | 'out';
|
|
113
|
+
|
|
114
|
+
/** Something the app does at a boundary — universal, serializable, tiny. */
|
|
115
|
+
export type ActionManifest = {
|
|
116
|
+
/** Stable identifier, e.g. `orders.list` or `orders.created.consume`. */
|
|
117
|
+
id: string;
|
|
118
|
+
plugin: string;
|
|
119
|
+
/** Adapter-owned binding name: `http`, `queue`, `cli`, `cron`, etc. */
|
|
120
|
+
binding: string;
|
|
121
|
+
/** `in` = world invokes/feeds the app; `out` = app emits. */
|
|
122
|
+
direction: ActionDirection;
|
|
123
|
+
/** Display-only address printed by `dot explain`. */
|
|
124
|
+
address?: string;
|
|
125
|
+
/** Human summary rendered into explain output and projected documents. */
|
|
126
|
+
summary?: string;
|
|
127
|
+
/** Adapter-owned, JSON-serializable detail. Opaque to the kernel. */
|
|
128
|
+
meta?: Readonly<JsonObject>;
|
|
129
|
+
/** Optional adapter-owned identifier+version for the meta layout. */
|
|
130
|
+
metaSchema?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Registered projection renderer for an adapter-owned document format. */
|
|
134
|
+
export type ProjectionManifest = {
|
|
135
|
+
/** Document format key matched by `dot explain --as <format>`. */
|
|
136
|
+
format: string;
|
|
137
|
+
/** Binding this projection primarily renders. Informational. */
|
|
138
|
+
binding: string;
|
|
139
|
+
/** Import specifier for the projection module. */
|
|
140
|
+
module: string;
|
|
141
|
+
/** Plugin that registered this projection. */
|
|
142
|
+
plugin: string;
|
|
143
|
+
};
|
|
144
|
+
|
|
42
145
|
/**
|
|
43
146
|
* Top-level manifest describing the static shape of a DOT app.
|
|
44
147
|
*
|
|
45
|
-
* Always carries the
|
|
148
|
+
* Always carries the six arrays — never omits any of them.
|
|
46
149
|
*/
|
|
47
150
|
export type DotAppManifest = {
|
|
151
|
+
/** Manifest schema version. 2 = actions/projections era. */
|
|
152
|
+
manifestVersion: 2;
|
|
48
153
|
app: {
|
|
49
154
|
name: string;
|
|
50
155
|
version?: string;
|
|
51
156
|
};
|
|
52
|
-
|
|
53
|
-
|
|
157
|
+
plugins: PluginManifest[];
|
|
158
|
+
actions: ActionManifest[];
|
|
54
159
|
services: ServiceManifest[];
|
|
55
160
|
lifecycle: LifecycleManifest[];
|
|
56
161
|
dependencies: DependencyEdge[];
|
|
162
|
+
projections: ProjectionManifest[];
|
|
57
163
|
};
|
|
58
164
|
|
|
59
|
-
/** Single
|
|
60
|
-
export type
|
|
165
|
+
/** Single plugin's declarative metadata. */
|
|
166
|
+
export type PluginManifest = {
|
|
61
167
|
name: string;
|
|
62
168
|
version?: string;
|
|
63
169
|
dependencies: readonly string[];
|
|
64
170
|
provides: readonly string[];
|
|
65
171
|
};
|
|
66
172
|
|
|
67
|
-
/** Single
|
|
68
|
-
export type RouteManifest = {
|
|
69
|
-
id: string;
|
|
70
|
-
pip: string;
|
|
71
|
-
method?: string;
|
|
72
|
-
path?: string;
|
|
73
|
-
transport: RouteTransport;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** Single service published by a pip. */
|
|
173
|
+
/** Single service published by a plugin. */
|
|
77
174
|
export type ServiceManifest = {
|
|
78
175
|
name: string;
|
|
79
|
-
|
|
176
|
+
plugin: string;
|
|
80
177
|
kind: ServiceKind;
|
|
81
178
|
};
|
|
82
179
|
|
|
83
|
-
/** Which lifecycle hooks a
|
|
180
|
+
/** Which lifecycle hooks a plugin participates in. */
|
|
84
181
|
export type LifecycleManifest = {
|
|
85
|
-
|
|
182
|
+
plugin: string;
|
|
86
183
|
hooks: readonly DotLifecycleHook[];
|
|
87
184
|
};
|
|
88
185
|
|
|
89
|
-
/** Directed edge in the
|
|
186
|
+
/** Directed edge in the plugin dependency graph. */
|
|
90
187
|
export type DependencyEdge = {
|
|
91
188
|
from: string;
|
|
92
189
|
to: string;
|