@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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * OpenTelemetry instrumentation for the DOT kernel.
3
+ *
4
+ * DOT is OTel-first: every lifecycle phase and every per-pip hook
5
+ * automatically emits a span and a duration histogram. When no SDK is
6
+ * registered, the OTel API returns no-op implementations and the kernel
7
+ * pays zero allocation per call — the discipline that makes "OTel-first"
8
+ * compatible with principle 5 (zero optional dependencies in the kernel).
9
+ *
10
+ * Consumers wire the SDK however they like (`@opentelemetry/sdk-node`,
11
+ * `@arki/meter`, etc.); the kernel never touches the SDK directly.
12
+ *
13
+ * Span / attribute conventions:
14
+ *
15
+ * - Phase span: `dot.app.<phase>` — one root span per `configure()` /
16
+ * `boot()` / `start()` / `stop()` / `dispose()` call. Children
17
+ * auto-link via async-context.
18
+ * - Hook span: `dot.pip.<hook>` — one child span per pip's hook
19
+ * execution. The pip name lives on `dot.pip.name`, not in the span
20
+ * name, so backends can aggregate by hook across pips.
21
+ * - Attributes (`dot.app.name`, `dot.pip.name`, `dot.pip.version`,
22
+ * `dot.hook`, `dot.pip.order`) follow the OTel convention of
23
+ * namespacing under the library's prefix.
24
+ *
25
+ * @see packages/dot/docs/principles.md — principle 3 (deterministic) +
26
+ * principle 4 (agent-discoverable).
27
+ */
28
+
29
+ import { metrics, SpanStatusCode, trace, type Span } from '@opentelemetry/api';
30
+ import type { Logger } from '@arki/log';
31
+
32
+ import type { DotLifecycleHook } from '../lifecycle.js';
33
+
34
+ const INSTRUMENTATION_NAME = '@arki/dot';
35
+ const INSTRUMENTATION_VERSION = '0.1.0';
36
+
37
+ /** OTel tracer for the DOT kernel. No-op when no SDK is registered. */
38
+ export const tracer = trace.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
39
+
40
+ /** OTel meter for the DOT kernel. No-op when no SDK is registered. */
41
+ export const meter = metrics.getMeter(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
42
+
43
+ /**
44
+ * Histogram recording per-pip per-hook execution duration in milliseconds.
45
+ *
46
+ * Attributes:
47
+ * - `dot.app.name` — app name
48
+ * - `dot.pip.name` — pip name
49
+ * - `dot.hook` — which lifecycle hook (`configure` | `boot` | ...)
50
+ * - `dot.status` — `ok` | `failed`
51
+ */
52
+ export const hookDurationHistogram = meter.createHistogram('dot.pip.hook.duration', {
53
+ description: 'Wall-clock duration of a single pip lifecycle hook execution.',
54
+ unit: 'ms',
55
+ });
56
+
57
+ /**
58
+ * Histogram recording per-phase (app-level) execution duration.
59
+ *
60
+ * Attributes:
61
+ * - `dot.app.name` — app name
62
+ * - `dot.app.phase` — which phase (`configure` | `boot` | `start` | `stop` | `dispose`)
63
+ * - `dot.status` — `ok` | `failed`
64
+ */
65
+ export const phaseDurationHistogram = meter.createHistogram('dot.app.phase.duration', {
66
+ description: 'Wall-clock duration of a complete app-level lifecycle phase.',
67
+ unit: 'ms',
68
+ });
69
+
70
+ /**
71
+ * Sanitize attribute values — OTel rejects `undefined` and strips out
72
+ * keys whose values aren't primitives or arrays of primitives. We drop
73
+ * `undefined` here so call sites can pass optional fields without
74
+ * branching.
75
+ */
76
+ function attrs(
77
+ source: Readonly<Record<string, string | number | boolean | undefined>>,
78
+ ): Record<string, string | number | boolean> {
79
+ const out: Record<string, string | number | boolean> = {};
80
+ for (const [k, v] of Object.entries(source)) {
81
+ if (v !== undefined) out[k] = v;
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** Stringify any thrown value for span status / exception recording. */
87
+ function stringifyForSpan(value: unknown): string {
88
+ if (value instanceof Error) return value.message;
89
+ if (typeof value === 'string') return value;
90
+ try {
91
+ return JSON.stringify(value);
92
+ } catch {
93
+ return String(value);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Wrap a single pip-hook execution in a child span and record its
99
+ * duration in the histogram. The span auto-links to the active phase
100
+ * span (set by {@link withPhaseSpan}) so traces show a clean parent-child
101
+ * hierarchy:
102
+ *
103
+ * dot.app.boot
104
+ * ├── dot.pip.boot (dot.pip.name=env, dot.hook=boot)
105
+ * ├── dot.pip.boot (dot.pip.name=db, dot.hook=boot)
106
+ * └── dot.pip.boot (dot.pip.name=kv, dot.hook=boot)
107
+ *
108
+ * Re-throws on failure after marking the span ERROR so the caller's
109
+ * existing error-handling path stays intact.
110
+ */
111
+ export function withPipHookSpan<R>(
112
+ opts: {
113
+ readonly appName: string;
114
+ readonly pipName: string;
115
+ readonly pipVersion?: string;
116
+ readonly hook: DotLifecycleHook;
117
+ readonly order: number;
118
+ /**
119
+ * Optional pip-scoped logger. When present, the helper threads the
120
+ * span's `traceId` + `spanId` onto the logger via `setTraceContext`
121
+ * before invoking the body — every log line emitted from within the
122
+ * pip's hook is automatically correlated with this span in the
123
+ * `traceId`/`spanId` fields of `LogRecord`.
124
+ */
125
+ readonly logger?: Logger;
126
+ },
127
+ fn: (span: Span) => R,
128
+ ): R {
129
+ const span = tracer.startSpan(`dot.pip.${opts.hook}`, {
130
+ attributes: attrs({
131
+ 'dot.app.name': opts.appName,
132
+ 'dot.pip.name': opts.pipName,
133
+ 'dot.pip.version': opts.pipVersion,
134
+ 'dot.hook': opts.hook,
135
+ 'dot.pip.order': opts.order,
136
+ }),
137
+ });
138
+ if (opts.logger) {
139
+ const sc = span.spanContext();
140
+ opts.logger.setTraceContext(sc.traceId, sc.spanId);
141
+ }
142
+ const started = performance.now();
143
+ let status: 'ok' | 'failed' = 'ok';
144
+ const finish = (): void => {
145
+ const durationMs = performance.now() - started;
146
+ hookDurationHistogram.record(durationMs, {
147
+ 'dot.app.name': opts.appName,
148
+ 'dot.pip.name': opts.pipName,
149
+ 'dot.hook': opts.hook,
150
+ 'dot.status': status,
151
+ });
152
+ span.end();
153
+ };
154
+ try {
155
+ const result = fn(span);
156
+ if (result instanceof Promise) {
157
+ return result.then(
158
+ v => {
159
+ span.setStatus({ code: SpanStatusCode.OK });
160
+ finish();
161
+ return v;
162
+ },
163
+ (e: unknown) => {
164
+ status = 'failed';
165
+ span.setStatus({ code: SpanStatusCode.ERROR, message: stringifyForSpan(e) });
166
+ if (e instanceof Error) span.recordException(e);
167
+ finish();
168
+ throw e;
169
+ },
170
+ ) as R;
171
+ }
172
+ span.setStatus({ code: SpanStatusCode.OK });
173
+ finish();
174
+ return result;
175
+ } catch (e) {
176
+ status = 'failed';
177
+ span.setStatus({ code: SpanStatusCode.ERROR, message: stringifyForSpan(e) });
178
+ if (e instanceof Error) span.recordException(e);
179
+ finish();
180
+ throw e;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Wrap an entire app-level phase (`configure`/`boot`/`start`/`stop`/`dispose`)
186
+ * in an active span. Children created by {@link withPipHookSpan} inherit
187
+ * this span as their parent via async-context.
188
+ *
189
+ * The callback's return value (sync or Promise) is propagated unchanged
190
+ * to preserve the kernel's existing control flow.
191
+ */
192
+ export function withPhaseSpan<R>(
193
+ opts: {
194
+ readonly appName: string;
195
+ readonly appVersion?: string;
196
+ readonly phase: DotLifecycleHook;
197
+ readonly pipCount: number;
198
+ /**
199
+ * Optional phase-scoped logger. When present, the helper threads
200
+ * the span's `traceId` + `spanId` onto the logger before invoking
201
+ * the body — every log line written during the phase carries the
202
+ * trace context so it's groupable with the corresponding span in
203
+ * any OTel-compatible backend.
204
+ */
205
+ readonly logger?: Logger;
206
+ },
207
+ fn: (span: Span) => R,
208
+ ): R {
209
+ return tracer.startActiveSpan(
210
+ `dot.app.${opts.phase}`,
211
+ {
212
+ attributes: attrs({
213
+ 'dot.app.name': opts.appName,
214
+ 'dot.app.version': opts.appVersion,
215
+ 'dot.app.phase': opts.phase,
216
+ 'dot.app.pip.count': opts.pipCount,
217
+ }),
218
+ },
219
+ (span): R => {
220
+ if (opts.logger) {
221
+ const sc = span.spanContext();
222
+ opts.logger.setTraceContext(sc.traceId, sc.spanId);
223
+ }
224
+ const started = performance.now();
225
+ let status: 'ok' | 'failed' = 'ok';
226
+ const finish = (): void => {
227
+ const durationMs = performance.now() - started;
228
+ phaseDurationHistogram.record(durationMs, {
229
+ 'dot.app.name': opts.appName,
230
+ 'dot.app.phase': opts.phase,
231
+ 'dot.status': status,
232
+ });
233
+ span.end();
234
+ };
235
+ try {
236
+ const result = fn(span);
237
+ if (result instanceof Promise) {
238
+ return result.then(
239
+ v => {
240
+ span.setStatus({ code: SpanStatusCode.OK });
241
+ finish();
242
+ return v;
243
+ },
244
+ (e: unknown) => {
245
+ status = 'failed';
246
+ span.setStatus({ code: SpanStatusCode.ERROR, message: stringifyForSpan(e) });
247
+ if (e instanceof Error) span.recordException(e);
248
+ finish();
249
+ throw e;
250
+ },
251
+ ) as R;
252
+ }
253
+ span.setStatus({ code: SpanStatusCode.OK });
254
+ finish();
255
+ return result;
256
+ } catch (e) {
257
+ status = 'failed';
258
+ span.setStatus({ code: SpanStatusCode.ERROR, message: stringifyForSpan(e) });
259
+ if (e instanceof Error) span.recordException(e);
260
+ finish();
261
+ throw e;
262
+ }
263
+ },
264
+ );
265
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Lifecycle observer surface for the DOT kernel.
3
+ *
4
+ * `DotLifecycleObserver` is the in-process companion to the OTel signals
5
+ * emitted by {@link withPhaseSpan} / {@link withPipHookSpan}. Where OTel
6
+ * is the contract for cross-process tracing (consumers register an SDK
7
+ * and ship spans to a backend), the observer is the contract for *local*
8
+ * programmatic inspection — used by tests, CLI tooling, ASCII waterfalls,
9
+ * and ad-hoc diagnostics that don't require an SDK to be registered.
10
+ *
11
+ * Both signals are emitted from the same kernel sites; neither is layered
12
+ * under the other. A consumer can use one, both, or neither — the kernel
13
+ * pays zero allocation for the observer fan-out when no observers are
14
+ * registered.
15
+ *
16
+ * @see packages/dot/docs/observability.md — consumer-facing surface
17
+ * @see packages/dot/docs/principles.md — principle 5 (zero optional deps)
18
+ */
19
+
20
+ import type { DotLifecycleHook } from './lifecycle.js';
21
+
22
+ /**
23
+ * The single fan-out event type emitted by the kernel to every registered
24
+ * observer. Discriminated by `kind`:
25
+ *
26
+ * - `'phase'` — boundary of a top-level lifecycle phase
27
+ * - `'pip-hook'` — boundary of a single pip's hook execution
28
+ *
29
+ * Status discriminates the boundary itself:
30
+ *
31
+ * - `'starting'` — emitted *before* the work begins
32
+ * - `'completed'` — emitted *after* the work finished successfully
33
+ * - `'failed'` — emitted *after* the work threw or rejected
34
+ *
35
+ * `starting` events never carry `durationMs` / `error`. `completed` events
36
+ * carry `durationMs`. `failed` events carry both `durationMs` and `error`.
37
+ */
38
+ export type DotLifecycleEvent = DotPhaseLifecycleEvent | DotPipHookLifecycleEvent;
39
+
40
+ /** Possible event statuses. See {@link DotLifecycleEvent}. */
41
+ export type DotLifecycleEventStatus = 'starting' | 'completed' | 'failed';
42
+
43
+ /** Boundary of a top-level lifecycle phase. */
44
+ export type DotPhaseLifecycleEvent = {
45
+ readonly kind: 'phase';
46
+ /** The phase the event belongs to. */
47
+ readonly phase: DotLifecycleHook;
48
+ readonly status: DotLifecycleEventStatus;
49
+ /** App name (so multiplexed observers can disambiguate). */
50
+ readonly appName: string;
51
+ /** Wall-clock duration in milliseconds. Present on `completed` / `failed`. */
52
+ readonly durationMs?: number;
53
+ /** The error value, if `status === 'failed'`. */
54
+ readonly error?: unknown;
55
+ /** Wall-clock timestamp in ms-since-epoch (`Date.now()` semantics). */
56
+ readonly timestamp: number;
57
+ };
58
+
59
+ /** Boundary of a single pip's hook execution. */
60
+ export type DotPipHookLifecycleEvent = {
61
+ readonly kind: 'pip-hook';
62
+ /** The phase the hook belongs to. */
63
+ readonly phase: DotLifecycleHook;
64
+ /** The pip whose hook is being executed. */
65
+ readonly pip: string;
66
+ /** Topological order of the pip within the phase (0-based). */
67
+ readonly order: number;
68
+ readonly status: DotLifecycleEventStatus;
69
+ readonly appName: string;
70
+ readonly durationMs?: number;
71
+ readonly error?: unknown;
72
+ readonly timestamp: number;
73
+ };
74
+
75
+ /**
76
+ * Observer function signature. Synchronous by contract — observers MUST
77
+ * NOT block the lifecycle. If a long-running side effect is needed
78
+ * (e.g. shipping events to a remote sink), the observer should buffer
79
+ * the event and process it on a separate cadence.
80
+ *
81
+ * Exceptions thrown by an observer are caught and dropped on the kernel
82
+ * floor (with a DEBUG log on `arki:dot:lifecycle`). One observer's
83
+ * misbehaviour can never cause another observer to miss an event, nor
84
+ * can it break the lifecycle.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { defineApp } from '@arki/dot';
89
+ *
90
+ * const events: DotLifecycleEvent[] = [];
91
+ * const app = await defineApp('my-app', {
92
+ * observers: [(event) => events.push(event)],
93
+ * })
94
+ * .use(myPip)
95
+ * .boot();
96
+ *
97
+ * // events now contains the full configure + boot event stream.
98
+ * ```
99
+ */
100
+ export type DotLifecycleObserver = (event: DotLifecycleEvent) => void;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Lifecycle primitives for the DOT kernel.
3
+ *
4
+ * The kernel uses a 5-hook lifecycle that runs in declaration order
5
+ * (providers are `.use()`d before their consumers — the app builder's
6
+ * type-level guard enforces this at compile time):
7
+ *
8
+ * - `configure` — synchronous registration of metadata, routes, services
9
+ * - `boot` — async open of resources, publishes services into `app.services`
10
+ * - `start` — async start of active work (workers, schedulers, listeners)
11
+ * - `stop` — async stop of active work, runs in reverse declaration order
12
+ * - `dispose` — async release of booted resources, runs in reverse declaration order
13
+ *
14
+ * Hook semantics, failure ordering, and idempotency rules are documented on the
15
+ * public `DotApp` interface in `./define-app.ts`.
16
+ */
17
+
18
+ /** Identifier of a single hook in the DOT lifecycle. */
19
+ export type DotLifecycleHook = 'configure' | 'boot' | 'start' | 'stop' | 'dispose';
20
+
21
+ /**
22
+ * The complete set of lifecycle hooks in execution order.
23
+ * `stop` and `dispose` run in reverse declaration order across pips, but the
24
+ * sequence of hooks themselves is always `configure -> boot -> start -> stop -> dispose`.
25
+ */
26
+ export const DOT_LIFECYCLE_HOOKS: readonly DotLifecycleHook[] = [
27
+ 'configure',
28
+ 'boot',
29
+ 'start',
30
+ 'stop',
31
+ 'dispose',
32
+ ] as const;
33
+
34
+ /**
35
+ * Macro-states the DotApp can occupy.
36
+ *
37
+ * State transitions:
38
+ *
39
+ * defined -> configured -> booted -> started -> stopped -> disposed
40
+ * |
41
+ * +-> disposed (skip stopped if never started)
42
+ *
43
+ * any-of(configure|boot|start) failure -> failed
44
+ *
45
+ * `failed` and `disposed` are terminal — callers must create a new instance.
46
+ */
47
+ export type DotLifecycleState = 'defined' | 'configured' | 'booted' | 'started' | 'stopped' | 'disposed' | 'failed';
48
+
49
+ /** Stable error codes for lifecycle failures. */
50
+ export const DotLifecycleErrorCode = {
51
+ /** A `configure` hook attempted async work (returned a Promise). */
52
+ ConfigureAsync: 'DOT_LIFECYCLE_E001',
53
+ /** A `configure` hook threw. */
54
+ ConfigureFailed: 'DOT_LIFECYCLE_E002',
55
+ /** A `boot` hook threw. */
56
+ BootFailed: 'DOT_LIFECYCLE_E003',
57
+ /** A `start` hook threw. */
58
+ StartFailed: 'DOT_LIFECYCLE_E004',
59
+ /** One or more `stop` hooks threw — aggregate. */
60
+ StopFailed: 'DOT_LIFECYCLE_E005',
61
+ /** One or more `dispose` hooks threw — aggregate. */
62
+ DisposeFailed: 'DOT_LIFECYCLE_E006',
63
+ /** Caller tried to reuse the app after dispose. */
64
+ ReuseAfterDispose: 'DOT_LIFECYCLE_E007',
65
+ /** Caller tried to reuse the app after a failed lifecycle. */
66
+ ReuseAfterFailure: 'DOT_LIFECYCLE_E008',
67
+ // E009 (DependencyCycle) and E010 (MissingDependency) are RETIRED with the
68
+ // v2 wiring model — declaration order is boot order, so cycles and
69
+ // name-based dependency declarations no longer exist. Codes are never
70
+ // reused for new meanings.
71
+ /** Pip registered twice. */
72
+ DuplicatePip: 'DOT_LIFECYCLE_E011',
73
+ /** A pip's `needs` entry has no provider among earlier-booted pips. */
74
+ UnsatisfiedNeed: 'DOT_LIFECYCLE_E012',
75
+ /** A pip published a wire key that an earlier pip already provides. */
76
+ ServiceCollision: 'DOT_LIFECYCLE_E013',
77
+ /** A needs alias, publish key, or rename target uses the reserved `$` prefix. */
78
+ ReservedServiceKey: 'DOT_LIFECYCLE_E014',
79
+ } as const;
80
+
81
+ export type DotLifecycleErrorCodeValue = (typeof DotLifecycleErrorCode)[keyof typeof DotLifecycleErrorCode];
82
+
83
+ /**
84
+ * Structured error thrown for any lifecycle failure or misuse.
85
+ *
86
+ * Carries:
87
+ * - `code` — stable machine-readable error code (see {@link DotLifecycleErrorCode}).
88
+ * - `phase` — which hook (or pseudo-hook) failed.
89
+ * - `pip` — which pip name, when applicable.
90
+ * - `cause` — original error if wrapped from a hook throw.
91
+ * - `failures` — for aggregate errors (stop/dispose), the per-pip failures.
92
+ */
93
+ export class DotLifecycleError extends Error {
94
+ readonly code: DotLifecycleErrorCodeValue;
95
+ readonly phase: DotLifecycleHook;
96
+ readonly pip?: string;
97
+ override readonly cause?: unknown;
98
+ readonly failures?: readonly DotLifecyclePipFailure[];
99
+
100
+ constructor(args: {
101
+ code: DotLifecycleErrorCodeValue;
102
+ phase: DotLifecycleHook;
103
+ message: string;
104
+ pip?: string;
105
+ cause?: unknown;
106
+ failures?: readonly DotLifecyclePipFailure[];
107
+ }) {
108
+ super(args.message);
109
+ this.name = 'DotLifecycleError';
110
+ this.code = args.code;
111
+ this.phase = args.phase;
112
+ this.pip = args.pip;
113
+ this.cause = args.cause;
114
+ this.failures = args.failures;
115
+ }
116
+ }
117
+
118
+ /** Single pip failure inside an aggregate lifecycle error (stop/dispose). */
119
+ export type DotLifecyclePipFailure = {
120
+ pip: string;
121
+ phase: DotLifecycleHook;
122
+ error: unknown;
123
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Manifest types for the DOT kernel.
3
+ *
4
+ * A `DotAppManifest` is the static, declarative description of an app: which
5
+ * pips are registered, what routes/services they contribute, and how they
6
+ * depend on each other. It is built up during the `configure` phase from
7
+ * registration calls and finalised once `configure` completes.
8
+ *
9
+ * CONTRACT: `DotAppManifest` always exposes the same five top-level arrays
10
+ * (`pips`, `routes`, `services`, `lifecycle`, `dependencies`). Consumers
11
+ * MUST NOT see an omitted array — empty is empty, but never missing.
12
+ * This shape is referenced by Task 7's scorecard.
13
+ */
14
+
15
+ import type { DotLifecycleHook } from './lifecycle.js';
16
+
17
+ /**
18
+ * Kind of service that a pip can publish.
19
+ *
20
+ * Well-known kinds map 1:1 to the canonical `@arki/*` adapters. `custom`
21
+ * is the escape hatch for pip-defined service kinds — use it whenever a
22
+ * pip publishes something that does not fit a reserved shape.
23
+ */
24
+ export type ServiceKind =
25
+ | 'env'
26
+ | 'db'
27
+ | 'kv'
28
+ | 'queue'
29
+ | 'auth'
30
+ | 'email'
31
+ | 'logger'
32
+ | 'event-store'
33
+ | 'message-bus'
34
+ | 'custom';
35
+
36
+ /** Transport that a route is exposed under. */
37
+ export type RouteTransport = 'http' | 'orpc' | 'trpc' | 'rpc' | 'custom';
38
+
39
+ /** Kind of dependency edge between two pips. */
40
+ export type DependencyEdgeKind = 'requires' | 'provides' | 'uses';
41
+
42
+ /**
43
+ * Top-level manifest describing the static shape of a DOT app.
44
+ *
45
+ * Always carries the five arrays — never omits any of them.
46
+ */
47
+ export type DotAppManifest = {
48
+ app: {
49
+ name: string;
50
+ version?: string;
51
+ };
52
+ pips: PipManifest[];
53
+ routes: RouteManifest[];
54
+ services: ServiceManifest[];
55
+ lifecycle: LifecycleManifest[];
56
+ dependencies: DependencyEdge[];
57
+ };
58
+
59
+ /** Single pip's declarative metadata. */
60
+ export type PipManifest = {
61
+ name: string;
62
+ version?: string;
63
+ dependencies: readonly string[];
64
+ provides: readonly string[];
65
+ };
66
+
67
+ /** Single route exposed by a pip. */
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. */
77
+ export type ServiceManifest = {
78
+ name: string;
79
+ pip: string;
80
+ kind: ServiceKind;
81
+ };
82
+
83
+ /** Which lifecycle hooks a pip participates in. */
84
+ export type LifecycleManifest = {
85
+ pip: string;
86
+ hooks: readonly DotLifecycleHook[];
87
+ };
88
+
89
+ /** Directed edge in the pip dependency graph. */
90
+ export type DependencyEdge = {
91
+ from: string;
92
+ to: string;
93
+ kind: DependencyEdgeKind;
94
+ };