@arki/dot 0.3.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.
Files changed (113) hide show
  1. package/README.md +86 -86
  2. package/dist/cli/discover.js +2 -2
  3. package/dist/cli/discover.js.map +1 -1
  4. package/dist/cli/error-codes.d.ts +8 -0
  5. package/dist/cli/error-codes.d.ts.map +1 -1
  6. package/dist/cli/error-codes.js +12 -0
  7. package/dist/cli/error-codes.js.map +1 -1
  8. package/dist/cli/index.d.ts +9 -2
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +54 -23
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/new.d.ts +1 -1
  13. package/dist/cli/new.js +1 -1
  14. package/dist/cli/render-doctor.d.ts +1 -1
  15. package/dist/cli/render-doctor.js +23 -23
  16. package/dist/cli/render-doctor.js.map +1 -1
  17. package/dist/cli/render-explain.d.ts.map +1 -1
  18. package/dist/cli/render-explain.js +33 -19
  19. package/dist/cli/render-explain.js.map +1 -1
  20. package/dist/cli/render-graph.d.ts +3 -3
  21. package/dist/cli/render-graph.js +12 -12
  22. package/dist/cli/render-graph.js.map +1 -1
  23. package/dist/cli/render-projection.d.ts +29 -0
  24. package/dist/cli/render-projection.d.ts.map +1 -0
  25. package/dist/cli/render-projection.js +202 -0
  26. package/dist/cli/render-projection.js.map +1 -0
  27. package/dist/define-app.d.ts +49 -17
  28. package/dist/define-app.d.ts.map +1 -1
  29. package/dist/define-app.js +16 -9
  30. package/dist/define-app.js.map +1 -1
  31. package/dist/diagnostics.d.ts +19 -15
  32. package/dist/diagnostics.d.ts.map +1 -1
  33. package/dist/diagnostics.js +2 -2
  34. package/dist/index.d.ts +15 -12
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +7 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/init-pips.d.ts +45 -0
  39. package/dist/init-pips.d.ts.map +1 -0
  40. package/dist/init-pips.js +49 -0
  41. package/dist/init-pips.js.map +1 -0
  42. package/dist/init-plugins.d.ts +45 -0
  43. package/dist/init-plugins.d.ts.map +1 -0
  44. package/dist/init-plugins.js +49 -0
  45. package/dist/init-plugins.js.map +1 -0
  46. package/dist/kernel/app-instance.d.ts +5 -5
  47. package/dist/kernel/app-instance.d.ts.map +1 -1
  48. package/dist/kernel/app-instance.js +375 -244
  49. package/dist/kernel/app-instance.js.map +1 -1
  50. package/dist/kernel/otel.d.ts +19 -19
  51. package/dist/kernel/otel.d.ts.map +1 -1
  52. package/dist/kernel/otel.js +22 -22
  53. package/dist/kernel/otel.js.map +1 -1
  54. package/dist/lifecycle-observer.d.ts +10 -10
  55. package/dist/lifecycle-observer.d.ts.map +1 -1
  56. package/dist/lifecycle-observer.js +1 -1
  57. package/dist/lifecycle.d.ts +18 -14
  58. package/dist/lifecycle.d.ts.map +1 -1
  59. package/dist/lifecycle.js +13 -9
  60. package/dist/lifecycle.js.map +1 -1
  61. package/dist/manifest.d.ts +70 -48
  62. package/dist/manifest.d.ts.map +1 -1
  63. package/dist/manifest.js +74 -5
  64. package/dist/manifest.js.map +1 -1
  65. package/dist/pip-contract.d.ts +29 -21
  66. package/dist/pip-contract.d.ts.map +1 -1
  67. package/dist/pip-contract.js +1 -0
  68. package/dist/pip-contract.js.map +1 -1
  69. package/dist/pip.d.ts +7 -4
  70. package/dist/pip.d.ts.map +1 -1
  71. package/dist/pip.js +2 -0
  72. package/dist/pip.js.map +1 -1
  73. package/dist/plugin-contract.d.ts +335 -143
  74. package/dist/plugin-contract.d.ts.map +1 -1
  75. package/dist/plugin-contract.js +201 -36
  76. package/dist/plugin-contract.js.map +1 -1
  77. package/dist/plugin.d.ts +18 -15
  78. package/dist/plugin.d.ts.map +1 -1
  79. package/dist/plugin.js +11 -11
  80. package/dist/plugin.js.map +1 -1
  81. package/dist/test-harness.d.ts +31 -31
  82. package/dist/test-harness.d.ts.map +1 -1
  83. package/dist/test-harness.js +29 -28
  84. package/dist/test-harness.js.map +1 -1
  85. package/dist/timeline.d.ts +2 -2
  86. package/dist/timeline.js +5 -5
  87. package/dist/timeline.js.map +1 -1
  88. package/package.json +9 -9
  89. package/src/cli/discover.ts +2 -2
  90. package/src/cli/error-codes.ts +12 -0
  91. package/src/cli/index.ts +76 -29
  92. package/src/cli/new.ts +1 -1
  93. package/src/cli/render-doctor.ts +21 -21
  94. package/src/cli/render-explain.ts +37 -19
  95. package/src/cli/render-graph.ts +12 -12
  96. package/src/cli/render-projection.ts +239 -0
  97. package/src/define-app.ts +85 -22
  98. package/src/diagnostics.ts +19 -15
  99. package/src/index.ts +37 -20
  100. package/src/init-plugins.ts +142 -0
  101. package/src/kernel/app-instance.ts +428 -282
  102. package/src/kernel/otel.ts +27 -27
  103. package/src/lifecycle-observer.ts +10 -10
  104. package/src/lifecycle.ts +19 -15
  105. package/src/manifest.ts +128 -50
  106. package/src/{pip-contract.ts → plugin-contract.ts} +73 -62
  107. package/src/{pip.ts → plugin.ts} +37 -18
  108. package/src/test-harness.ts +52 -51
  109. package/src/timeline.ts +6 -6
  110. package/templates/app-minimal/AGENTS.md.tmpl +9 -9
  111. package/templates/app-minimal/README.md.tmpl +1 -1
  112. package/templates/app-minimal/tests/boot.test.ts.tmpl +1 -1
  113. package/src/cli/render-openapi.ts +0 -147
@@ -9,56 +9,61 @@ import { Logger } from '@arki/log';
9
9
  import { createDebugLogger } from '@arki/log/debug';
10
10
 
11
11
  import type {
12
+ ActionDiagnostic,
12
13
  DiagnosticIssue,
13
14
  DotDiagnosticsSnapshot,
14
15
  LifecycleDiagnostic,
15
- PipDiagnostic,
16
- RouteDiagnostic,
16
+ PluginDiagnostic,
17
17
  ServiceDiagnostic,
18
18
  } from '../diagnostics.js';
19
19
  import type { DotLifecycleEvent, DotLifecycleObserver } from '../lifecycle-observer.js';
20
- import type { DotLifecycleHook, DotLifecyclePipFailure, DotLifecycleState } from '../lifecycle.js';
20
+ import type { DotLifecycleHook, DotLifecyclePluginFailure, DotLifecycleState } from '../lifecycle.js';
21
21
  import type {
22
+ ActionManifest,
22
23
  DependencyEdge,
23
24
  DotAppManifest,
25
+ JsonObject,
24
26
  LifecycleManifest,
25
- PipManifest,
26
- RouteManifest,
27
+ PluginManifest,
28
+ ProjectionManifest,
27
29
  ServiceManifest,
28
30
  } from '../manifest.js';
29
- import type { AnyPip, DotConfigureContext, Lazy, ServiceRecord } from '../pip-contract.js';
31
+ import type { AnyPlugin, DotConfigureContext, Lazy, ServiceRecord } from '../plugin-contract.js';
30
32
  import { DotLifecycleError, DotLifecycleErrorCode } from '../lifecycle.js';
31
- import { isLazy, isLazyWitness, lazyOf } from '../pip-contract.js';
32
- import { withPhaseSpan, withPipHookSpan } from './otel.js';
33
+ import { toJsonObject } from '../manifest.js';
34
+ import { isLazy, isLazyWitness, lazyOf } from '../plugin-contract.js';
35
+ import { withPhaseSpan, withPluginHookSpan } from './otel.js';
33
36
 
34
37
  const debugKernel = createDebugLogger('arki:dot:kernel');
35
38
 
36
39
  const DOCS_BASE = 'https://docs.arki.dev/dot/diagnostics';
37
40
 
38
- /** Per-pip mutable bookkeeping. */
39
- type PipRecord = {
40
- pip: AnyPip;
41
+ /** Per-plugin mutable bookkeeping. */
42
+ type PluginRecord = {
43
+ plugin: AnyPlugin;
41
44
  /** Declaration order (0-based) — v2 boot order IS declaration order. */
42
45
  order: number;
43
- /** Routes registered during `configure`. */
44
- routes: RouteManifest[];
46
+ /** Actions declared statically or registered during `configure`. */
47
+ actions: ActionManifest[];
48
+ /** Projection renderers registered during `configure`. */
49
+ projections: ProjectionManifest[];
45
50
  /** Services declared during `configure`. */
46
51
  services: ServiceManifest[];
47
- /** Lifecycle hooks the pip participates in. */
52
+ /** Lifecycle hooks the plugin participates in. */
48
53
  hooks: Set<DotLifecycleHook>;
49
54
  /** Provides capability strings declared during `configure`, joined with published wire keys. */
50
55
  provides: Set<string>;
51
56
  /**
52
- * Services published by this pip, keyed by LOCAL publish keys (pre-rename).
53
- * The pip's own stop/dispose contexts read these; the app-facing service
57
+ * Services published by this plugin, keyed by LOCAL publish keys (pre-rename).
58
+ * The plugin's own stop/dispose contexts read these; the app-facing service
54
59
  * map uses the renamed wire keys.
55
60
  */
56
61
  publishedServices: ServiceRecord;
57
- /** Whether the pip's `boot` hook completed successfully. */
62
+ /** Whether the plugin's `boot` hook completed successfully. */
58
63
  booted: boolean;
59
- /** Whether the pip's `start` hook completed successfully. */
64
+ /** Whether the plugin's `start` hook completed successfully. */
60
65
  started: boolean;
61
- /** Diagnostic issues collected for this pip. */
66
+ /** Diagnostic issues collected for this plugin. */
62
67
  issues: DiagnosticIssue[];
63
68
  /** Per-hook diagnostic entries. */
64
69
  lifecycleDiagnostics: LifecycleDiagnostic[];
@@ -75,7 +80,7 @@ function wireKeyOf(witness: object, alias: string): string {
75
80
  function makeIssue(args: {
76
81
  code: string;
77
82
  severity?: 'info' | 'warning' | 'error';
78
- pip?: string;
83
+ plugin?: string;
79
84
  message: string;
80
85
  remediation: string;
81
86
  docsAnchor: string;
@@ -84,7 +89,7 @@ function makeIssue(args: {
84
89
  return {
85
90
  code: args.code,
86
91
  severity: args.severity ?? 'error',
87
- pip: args.pip,
92
+ plugin: args.plugin,
88
93
  message: args.message,
89
94
  remediation: args.remediation,
90
95
  docsUrl: `${DOCS_BASE}#${args.docsAnchor}`,
@@ -95,7 +100,7 @@ function makeIssue(args: {
95
100
  export type DotAppInternalConfig = {
96
101
  appName: string;
97
102
  appVersion?: string;
98
- pips: readonly AnyPip[];
103
+ plugins: readonly AnyPlugin[];
99
104
  /** Runtime config bag passed to every `boot` hook. */
100
105
  config?: Readonly<Record<string, unknown>>;
101
106
  /**
@@ -117,9 +122,9 @@ export class DotAppImpl {
117
122
  readonly #appVersion: string | undefined;
118
123
  readonly #config: Readonly<Record<string, unknown>>;
119
124
 
120
- /** Pips in declaration order — v2 boot order IS declaration order. */
121
- readonly #ordered: readonly AnyPip[];
122
- readonly #records: Map<string, PipRecord>;
125
+ /** Plugins in declaration order — v2 boot order IS declaration order. */
126
+ readonly #ordered: readonly AnyPlugin[];
127
+ readonly #records: Map<string, PluginRecord>;
123
128
 
124
129
  /** Macro-state of the app. */
125
130
  #state: DotLifecycleState = 'defined';
@@ -130,10 +135,10 @@ export class DotAppImpl {
130
135
  /** Manifest finalised after `configure`. */
131
136
  #manifest: DotAppManifest;
132
137
 
133
- /** Wire-keyed services map — populated as pips boot. */
138
+ /** Wire-keyed services map — populated as plugins boot. */
134
139
  readonly #serviceMap = new Map<string, unknown>();
135
140
 
136
- /** Which pip provided each wire key — feeds dependency edges + collisions. */
141
+ /** Which plugin provided each wire key — feeds dependency edges + collisions. */
137
142
  readonly #providerByWireKey = new Map<string, string>();
138
143
 
139
144
  /** Dependency edges observed during boot (consumer → provider). */
@@ -167,7 +172,7 @@ export class DotAppImpl {
167
172
  * Structured lifecycle logger. One per app instance; named so consumers
168
173
  * can elevate it to DEBUG via `DEBUG=arki:dot:lifecycle` without
169
174
  * touching unrelated namespaces. Every line carries the app name plus
170
- * any phase/pip attributes from the call site. The span helpers
175
+ * any phase/plugin attributes from the call site. The span helpers
171
176
  * (see `./otel.ts`) thread the active span's `traceId`+`spanId` onto
172
177
  * forked instances of this logger, so every log record is groupable
173
178
  * with its trace in any OTel-compatible backend.
@@ -192,50 +197,51 @@ export class DotAppImpl {
192
197
 
193
198
  // Declaration order is boot order in v2. Duplicate names are surfaced at
194
199
  // construction (the `configure` phase pseudo-time).
195
- debugKernel('[%s] registering %d pip(s) in declaration order', this.#appName, config.pips.length);
196
- this.#ordered = [...config.pips];
200
+ debugKernel('[%s] registering %d plugin(s) in declaration order', this.#appName, config.plugins.length);
201
+ this.#ordered = [...config.plugins];
197
202
 
198
203
  this.#records = new Map();
199
- for (const [order, pip] of this.#ordered.entries()) {
200
- if (this.#records.has(pip.name)) {
204
+ for (const [order, plugin] of this.#ordered.entries()) {
205
+ if (this.#records.has(plugin.name)) {
201
206
  throw new DotLifecycleError({
202
- code: DotLifecycleErrorCode.DuplicatePip,
207
+ code: DotLifecycleErrorCode.DuplicatePlugin,
203
208
  phase: 'configure',
204
- pip: pip.name,
205
- message: `Pip "${pip.name}" is registered twice`,
209
+ plugin: plugin.name,
210
+ message: `Plugin "${plugin.name}" is registered twice`,
206
211
  });
207
212
  }
208
- // `$` is the kernel context namespace ($app/$pip/$config). The pip()
213
+ // `$` is the kernel context namespace ($app/$plugin/$config). The plugin()
209
214
  // constraint bans these at compile time; this is the runtime backstop
210
- // for erased pips and rename() targets, which types cannot see.
211
- for (const alias of Object.keys(pip.needs)) {
215
+ // for erased plugins and rename() targets, which types cannot see.
216
+ for (const alias of Object.keys(plugin.needs)) {
212
217
  if (alias.startsWith('$')) {
213
218
  throw new DotLifecycleError({
214
219
  code: DotLifecycleErrorCode.ReservedServiceKey,
215
220
  phase: 'configure',
216
- pip: pip.name,
221
+ plugin: plugin.name,
217
222
  message:
218
- `Pip "${pip.name}" declares needs alias "${alias}" — the "$" prefix is reserved ` +
219
- `for kernel context keys ($app, $pip, $config). Rename the alias.`,
223
+ `Plugin "${plugin.name}" declares needs alias "${alias}" — the "$" prefix is reserved ` +
224
+ `for kernel context keys ($app, $plugin, $config). Rename the alias.`,
220
225
  });
221
226
  }
222
227
  }
223
- for (const [localKey, wireKey] of Object.entries(pip.renames)) {
228
+ for (const [localKey, wireKey] of Object.entries(plugin.renames)) {
224
229
  if (wireKey.startsWith('$')) {
225
230
  throw new DotLifecycleError({
226
231
  code: DotLifecycleErrorCode.ReservedServiceKey,
227
232
  phase: 'configure',
228
- pip: pip.name,
233
+ plugin: plugin.name,
229
234
  message:
230
- `Pip "${pip.name}" renames "${localKey}" to "${wireKey}" — the "$" prefix is ` +
231
- `reserved for kernel context keys ($app, $pip, $config). Pick a different wire key.`,
235
+ `Plugin "${plugin.name}" renames "${localKey}" to "${wireKey}" — the "$" prefix is ` +
236
+ `reserved for kernel context keys ($app, $plugin, $config). Pick a different wire key.`,
232
237
  });
233
238
  }
234
239
  }
235
- this.#records.set(pip.name, {
236
- pip,
240
+ this.#records.set(plugin.name, {
241
+ plugin,
237
242
  order,
238
- routes: [],
243
+ actions: [],
244
+ projections: [],
239
245
  services: [],
240
246
  hooks: new Set<DotLifecycleHook>(),
241
247
  provides: new Set<string>(),
@@ -300,16 +306,16 @@ export class DotAppImpl {
300
306
  */
301
307
  #emitHook(
302
308
  phase: DotLifecycleHook,
303
- pip: string,
309
+ plugin: string,
304
310
  order: number,
305
311
  status: 'starting' | 'completed' | 'failed',
306
312
  opts?: { durationMs?: number; error?: unknown },
307
313
  ): void {
308
314
  if (this.#observers.size === 0) return;
309
315
  const event = {
310
- kind: 'pip-hook' as const,
316
+ kind: 'plugin-hook' as const,
311
317
  phase,
312
- pip,
318
+ plugin,
313
319
  order,
314
320
  status,
315
321
  appName: this.#appName,
@@ -320,6 +326,96 @@ export class DotAppImpl {
320
326
  this.#emit(event);
321
327
  }
322
328
 
329
+ #registerAction(record: PluginRecord, action: Omit<ActionManifest, 'plugin'>): void {
330
+ let meta: Readonly<JsonObject> | undefined;
331
+ if (action.meta !== undefined) {
332
+ try {
333
+ meta = toJsonObject(action.meta);
334
+ } catch (error) {
335
+ const issue = makeIssue({
336
+ code: DotLifecycleErrorCode.ActionMetaNotJson,
337
+ plugin: record.plugin.name,
338
+ message:
339
+ `action "${action.id}" for binding "${action.binding}" has metadata that is not JSON-round-trip safe: ` +
340
+ stringifyError(error),
341
+ remediation:
342
+ 'Only put JSON-serializable data in action meta. Convert Dates to strings, remove functions/undefined fields, and avoid NaN/Infinity.',
343
+ docsAnchor: 'action-meta-not-json',
344
+ metadata: { actionId: action.id, binding: action.binding },
345
+ });
346
+ record.issues.push(issue);
347
+ }
348
+ }
349
+
350
+ const stamped: ActionManifest = {
351
+ id: action.id,
352
+ plugin: record.plugin.name,
353
+ binding: action.binding,
354
+ direction: action.direction,
355
+ ...(action.address === undefined ? {} : { address: action.address }),
356
+ ...(action.summary === undefined ? {} : { summary: action.summary }),
357
+ ...(meta === undefined ? {} : { meta }),
358
+ ...(action.metaSchema === undefined ? {} : { metaSchema: action.metaSchema }),
359
+ };
360
+
361
+ for (const existingRecord of this.#records.values()) {
362
+ const duplicate = existingRecord.actions.find(
363
+ existing => existing.binding === stamped.binding && existing.id === stamped.id,
364
+ );
365
+ if (duplicate === undefined) continue;
366
+ const issue = makeIssue({
367
+ code: DotLifecycleErrorCode.DuplicateAction,
368
+ plugin: record.plugin.name,
369
+ message: `action "${stamped.id}" is registered more than once for binding "${stamped.binding}".`,
370
+ remediation:
371
+ 'Action ids must be unique within a binding. Rename one action or move one of them to a different binding.',
372
+ docsAnchor: 'duplicate-action',
373
+ metadata: { binding: stamped.binding, id: stamped.id, firstPlugin: duplicate.plugin },
374
+ });
375
+ record.issues.push(issue);
376
+ break;
377
+ }
378
+
379
+ record.actions.push(stamped);
380
+ }
381
+
382
+ #registerProjection(record: PluginRecord, projection: Omit<ProjectionManifest, 'plugin'>): void {
383
+ record.projections.push({ ...projection, plugin: record.plugin.name });
384
+ }
385
+
386
+ #registerRouteShim(
387
+ record: PluginRecord,
388
+ route: Parameters<DotConfigureContext['registerRoute']>[0],
389
+ ): void {
390
+ const meta: JsonObject = {};
391
+ if (route.method !== undefined) meta['method'] = route.method;
392
+ if (route.path !== undefined) meta['path'] = route.path;
393
+ if (route.input !== undefined) {
394
+ const input: JsonObject = {};
395
+ if (route.input.query !== undefined) input['query'] = route.input.query;
396
+ if (route.input.body !== undefined) input['body'] = route.input.body;
397
+ if (Object.keys(input).length > 0) meta['input'] = input;
398
+ }
399
+ if (route.output !== undefined) meta['output'] = route.output;
400
+ if (route.streaming !== undefined) meta['streaming'] = route.streaming;
401
+
402
+ this.#registerAction(record, {
403
+ id: route.id,
404
+ binding: route.transport,
405
+ direction: 'in',
406
+ ...(route.path === undefined ? {} : { address: `${route.method ?? 'GET'} ${route.path}` }),
407
+ ...(route.description === undefined ? {} : { summary: route.description }),
408
+ ...(Object.keys(meta).length === 0 ? {} : { meta }),
409
+ });
410
+ }
411
+
412
+ #actionFromSource(source: AnyPlugin['actions'][number]): Omit<ActionManifest, 'plugin'> {
413
+ if (hasToDotAction(source)) {
414
+ return source.toDotAction();
415
+ }
416
+ return source;
417
+ }
418
+
323
419
  /**
324
420
  * Wrap a sync phase body with starting/completed/failed observer events.
325
421
  * `configure` is the only sync phase in the kernel today — keep the
@@ -424,14 +520,14 @@ export class DotAppImpl {
424
520
  }
425
521
 
426
522
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'configure');
427
- phaseLogger.debug('configure: starting', { 'dot.app.pip.count': this.#ordered.length });
523
+ phaseLogger.debug('configure: starting', { 'dot.app.plugin.count': this.#ordered.length });
428
524
 
429
525
  this.#withPhaseEmit('configure', () => {
430
526
  withPhaseSpan(
431
527
  {
432
528
  appName: this.#appName,
433
529
  phase: 'configure',
434
- pipCount: this.#ordered.length,
530
+ pluginCount: this.#ordered.length,
435
531
  logger: phaseLogger,
436
532
  },
437
533
  () => this.#runConfigureInner(phaseLogger),
@@ -445,24 +541,57 @@ export class DotAppImpl {
445
541
  * intricate error-handling readable.
446
542
  */
447
543
  #runConfigureInner(phaseLogger: Logger): void {
448
- for (const pip of this.#ordered) {
449
- const record = this.#records.get(pip.name)!;
450
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', pip.name);
451
- if (!pip.hooks.configure) {
452
- pipLogger.debug('configure: skipped (no hook)');
544
+ for (const plugin of this.#ordered) {
545
+ const record = this.#records.get(plugin.name)!;
546
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
547
+ const declaredStarted = performance.now();
548
+ try {
549
+ for (const source of plugin.actions) this.#registerAction(record, this.#actionFromSource(source));
550
+ } catch (error) {
551
+ const durationMs = performance.now() - declaredStarted;
552
+ const issue = makeIssue({
553
+ code: DotLifecycleErrorCode.ConfigureFailed,
554
+ plugin: plugin.name,
555
+ message: `action declaration threw for plugin "${plugin.name}": ${stringifyError(error)}`,
556
+ remediation: `Fix the action declarations of "${plugin.name}". Declarative actions must be static and synchronous.`,
557
+ docsAnchor: 'configure-failed',
558
+ });
559
+ record.issues.push(issue);
560
+ record.lifecycleDiagnostics.push({
561
+ plugin: plugin.name,
562
+ hook: 'configure',
563
+ state: 'failed',
564
+ order: record.order,
565
+ durationMs,
566
+ issues: [issue],
567
+ });
568
+ this.#emitHook('configure', plugin.name, record.order, 'failed', { durationMs, error });
569
+ this.#state = 'failed';
570
+ this.#manifest = this.#buildManifest();
571
+ throw new DotLifecycleError({
572
+ code: DotLifecycleErrorCode.ConfigureFailed,
573
+ phase: 'configure',
574
+ plugin: plugin.name,
575
+ message: issue.message,
576
+ cause: error,
577
+ });
578
+ }
579
+
580
+ if (!plugin.hooks.configure) {
581
+ pluginLogger.debug('configure: skipped (no hook)');
453
582
  continue;
454
583
  }
455
584
  record.hooks.add('configure');
456
585
 
457
586
  const ctx: DotConfigureContext = {
458
- pipName: pip.name,
587
+ pluginName: plugin.name,
459
588
  appName: this.#appName,
460
589
  registerService: (name, kind) => {
461
- record.services.push({ name, pip: pip.name, kind });
462
- },
463
- registerRoute: route => {
464
- record.routes.push({ ...route, pip: pip.name });
590
+ record.services.push({ name, plugin: plugin.name, kind });
465
591
  },
592
+ registerAction: action => this.#registerAction(record, action),
593
+ registerProjection: projection => this.#registerProjection(record, projection),
594
+ registerRoute: route => this.#registerRouteShim(record, route),
466
595
  registerLifecycleHook: hook => {
467
596
  record.hooks.add(hook);
468
597
  },
@@ -472,45 +601,45 @@ export class DotAppImpl {
472
601
  };
473
602
 
474
603
  const started = performance.now();
475
- this.#emitHook('configure', pip.name, record.order, 'starting');
604
+ this.#emitHook('configure', plugin.name, record.order, 'starting');
476
605
  let returned: unknown;
477
606
  try {
478
- returned = withPipHookSpan(
607
+ returned = withPluginHookSpan(
479
608
  {
480
609
  appName: this.#appName,
481
- pipName: pip.name,
482
- pipVersion: pip.version,
610
+ pluginName: plugin.name,
611
+ pluginVersion: plugin.version,
483
612
  hook: 'configure',
484
613
  order: record.order,
485
- logger: pipLogger,
614
+ logger: pluginLogger,
486
615
  },
487
- () => pip.hooks.configure!(ctx),
616
+ () => plugin.hooks.configure!(ctx),
488
617
  );
489
618
  } catch (error) {
490
619
  const durationMs = performance.now() - started;
491
620
  const issue = makeIssue({
492
621
  code: DotLifecycleErrorCode.ConfigureFailed,
493
- pip: pip.name,
494
- message: `configure hook threw for pip "${pip.name}": ${stringifyError(error)}`,
495
- remediation: `Fix the error in the configure() hook of "${pip.name}". configure() is for synchronous registration only — avoid throwing on declarative work.`,
622
+ plugin: plugin.name,
623
+ message: `configure hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
624
+ remediation: `Fix the error in the configure() hook of "${plugin.name}". configure() is for synchronous registration only — avoid throwing on declarative work.`,
496
625
  docsAnchor: 'configure-failed',
497
626
  });
498
627
  record.issues.push(issue);
499
628
  record.lifecycleDiagnostics.push({
500
- pip: pip.name,
629
+ plugin: plugin.name,
501
630
  hook: 'configure',
502
631
  state: 'failed',
503
632
  order: record.order,
504
633
  durationMs,
505
634
  issues: [issue],
506
635
  });
507
- this.#emitHook('configure', pip.name, record.order, 'failed', { durationMs, error });
636
+ this.#emitHook('configure', plugin.name, record.order, 'failed', { durationMs, error });
508
637
  this.#state = 'failed';
509
638
  this.#manifest = this.#buildManifest();
510
639
  throw new DotLifecycleError({
511
640
  code: DotLifecycleErrorCode.ConfigureFailed,
512
641
  phase: 'configure',
513
- pip: pip.name,
642
+ plugin: plugin.name,
514
643
  message: issue.message,
515
644
  cause: error,
516
645
  });
@@ -520,21 +649,21 @@ export class DotAppImpl {
520
649
  const durationMs = performance.now() - started;
521
650
  const issue = makeIssue({
522
651
  code: DotLifecycleErrorCode.ConfigureAsync,
523
- pip: pip.name,
524
- message: `configure hook of pip "${pip.name}" returned a Promise. configure must be synchronous.`,
652
+ plugin: plugin.name,
653
+ message: `configure hook of plugin "${plugin.name}" returned a Promise. configure must be synchronous.`,
525
654
  remediation: 'Move async work to the boot() hook. configure() is for synchronous registration only.',
526
655
  docsAnchor: 'configure-async',
527
656
  });
528
657
  record.issues.push(issue);
529
658
  record.lifecycleDiagnostics.push({
530
- pip: pip.name,
659
+ plugin: plugin.name,
531
660
  hook: 'configure',
532
661
  state: 'failed',
533
662
  order: record.order,
534
663
  durationMs,
535
664
  issues: [issue],
536
665
  });
537
- this.#emitHook('configure', pip.name, record.order, 'failed', {
666
+ this.#emitHook('configure', plugin.name, record.order, 'failed', {
538
667
  durationMs,
539
668
  error: new Error(issue.message),
540
669
  });
@@ -543,38 +672,38 @@ export class DotAppImpl {
543
672
  throw new DotLifecycleError({
544
673
  code: DotLifecycleErrorCode.ConfigureAsync,
545
674
  phase: 'configure',
546
- pip: pip.name,
675
+ plugin: plugin.name,
547
676
  message: issue.message,
548
677
  });
549
678
  }
550
679
 
551
680
  const durationMs = performance.now() - started;
552
681
  record.lifecycleDiagnostics.push({
553
- pip: pip.name,
682
+ plugin: plugin.name,
554
683
  hook: 'configure',
555
684
  state: 'configured',
556
685
  order: record.order,
557
686
  durationMs,
558
687
  issues: [],
559
688
  });
560
- this.#emitHook('configure', pip.name, record.order, 'completed', { durationMs });
561
- pipLogger.debug('configure: done', { 'dot.pip.duration.ms': durationMs });
689
+ this.#emitHook('configure', plugin.name, record.order, 'completed', { durationMs });
690
+ pluginLogger.debug('configure: done', { 'dot.plugin.duration.ms': durationMs });
562
691
  }
563
692
 
564
693
  // Also declare lifecycle-hook participation for non-configure hooks present
565
- // on the pip object, so the manifest reflects them.
566
- for (const pip of this.#ordered) {
567
- const record = this.#records.get(pip.name)!;
568
- if (pip.hooks.boot) record.hooks.add('boot');
569
- if (pip.hooks.start) record.hooks.add('start');
570
- if (pip.hooks.stop) record.hooks.add('stop');
571
- if (pip.hooks.dispose) record.hooks.add('dispose');
694
+ // on the plugin object, so the manifest reflects them.
695
+ for (const plugin of this.#ordered) {
696
+ const record = this.#records.get(plugin.name)!;
697
+ if (plugin.hooks.boot) record.hooks.add('boot');
698
+ if (plugin.hooks.start) record.hooks.add('start');
699
+ if (plugin.hooks.stop) record.hooks.add('stop');
700
+ if (plugin.hooks.dispose) record.hooks.add('dispose');
572
701
  }
573
702
 
574
703
  this.#configured = true;
575
704
  this.#state = 'configured';
576
705
  this.#manifest = this.#buildManifest();
577
- phaseLogger.debug('configure: complete', { 'dot.app.pip.count': this.#ordered.length });
706
+ phaseLogger.debug('configure: complete', { 'dot.app.plugin.count': this.#ordered.length });
578
707
  }
579
708
 
580
709
  /**
@@ -617,14 +746,14 @@ export class DotAppImpl {
617
746
  }
618
747
 
619
748
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'boot');
620
- phaseLogger.debug('boot: starting', { 'dot.app.pip.count': this.#ordered.length });
749
+ phaseLogger.debug('boot: starting', { 'dot.app.plugin.count': this.#ordered.length });
621
750
 
622
751
  return this.#withPhaseEmitAsync('boot', () =>
623
752
  withPhaseSpan(
624
753
  {
625
754
  appName: this.#appName,
626
755
  phase: 'boot',
627
- pipCount: this.#ordered.length,
756
+ pluginCount: this.#ordered.length,
628
757
  logger: phaseLogger,
629
758
  },
630
759
  () => this.#runBootInner(phaseLogger),
@@ -633,34 +762,34 @@ export class DotAppImpl {
633
762
  }
634
763
 
635
764
  /**
636
- * Resolve a pip's needs into a hook context (alias-keyed), joined with
637
- * the `$`-prefixed kernel keys and — for post-boot hooks — the pip's own
765
+ * Resolve a plugin's needs into a hook context (alias-keyed), joined with
766
+ * the `$`-prefixed kernel keys and — for post-boot hooks — the plugin's own
638
767
  * published services (local keys).
639
768
  *
640
769
  * @throws {DotLifecycleError} `DOT_LIFECYCLE_E012` when a need has no
641
- * provider among earlier-booted pips. Teardown hooks can never hit
770
+ * provider among earlier-booted plugins. Teardown hooks can never hit
642
771
  * this: the service map only grows, and reverse-order teardown keeps
643
772
  * providers alive until their consumers are done.
644
773
  */
645
- #buildHookCtx(record: PipRecord, phase: DotLifecycleHook, includeOwnProvides: boolean): Record<string, unknown> {
774
+ #buildHookCtx(record: PluginRecord, phase: DotLifecycleHook, includeOwnProvides: boolean): Record<string, unknown> {
646
775
  const ctx: Record<string, unknown> = {
647
776
  $app: this.#appName,
648
- $pip: record.pip.name,
777
+ $plugin: record.plugin.name,
649
778
  $config: this.#config,
650
779
  };
651
- for (const [alias, witness] of Object.entries(record.pip.needs)) {
780
+ for (const [alias, witness] of Object.entries(record.plugin.needs)) {
652
781
  const wireKey = wireKeyOf(witness, alias);
653
782
  if (!this.#serviceMap.has(wireKey)) {
654
783
  throw new DotLifecycleError({
655
784
  code: DotLifecycleErrorCode.UnsatisfiedNeed,
656
785
  phase,
657
- pip: record.pip.name,
786
+ plugin: record.plugin.name,
658
787
  message:
659
- `Pip "${record.pip.name}" needs service "${wireKey}" but no earlier pip provides it. ` +
660
- `Register a provider with .use() before this pip. Services flow strictly forward — ` +
661
- `if a later pip provides "${wireKey}", move that provider earlier; if two pips need ` +
788
+ `Plugin "${record.plugin.name}" needs service "${wireKey}" but no earlier plugin provides it. ` +
789
+ `Register a provider with .use() before this plugin. Services flow strictly forward — ` +
790
+ `if a later plugin provides "${wireKey}", move that provider earlier; if two plugins need ` +
662
791
  `each other's services, that is a cycle: merge them or extract the shared piece into ` +
663
- `a third pip both consume.`,
792
+ `a third plugin both consume.`,
664
793
  });
665
794
  }
666
795
  const value = this.#serviceMap.get(wireKey);
@@ -671,8 +800,8 @@ export class DotAppImpl {
671
800
  // the underlying value's lifecycle stays with its provider.
672
801
  ctx[alias] = isLazyWitness(witness) && !isLazy(value) ? lazyOf(value) : value;
673
802
  const provider = this.#providerByWireKey.get(wireKey);
674
- if (provider !== undefined && !this.#wiringEdges.some(e => e.from === record.pip.name && e.to === provider)) {
675
- this.#wiringEdges.push({ from: record.pip.name, to: provider, kind: 'requires' });
803
+ if (provider !== undefined && !this.#wiringEdges.some(e => e.from === record.plugin.name && e.to === provider)) {
804
+ this.#wiringEdges.push({ from: record.plugin.name, to: provider, kind: 'requires' });
676
805
  }
677
806
  }
678
807
  if (includeOwnProvides) {
@@ -684,13 +813,13 @@ export class DotAppImpl {
684
813
  /**
685
814
  * Race one hook invocation against the app's `hookTimeoutMs` watchdog.
686
815
  * No budget configured → plain pass-through. On timeout the returned
687
- * promise rejects with `DOT_LIFECYCLE_E015` naming the pip and hook,
816
+ * promise rejects with `DOT_LIFECYCLE_E015` naming the plugin and hook,
688
817
  * and the call site's existing catch path applies the phase's normal
689
818
  * failure rules (boot rollback, start cascade, teardown aggregation).
690
819
  * The hook's own promise cannot be cancelled — the watchdog makes a
691
820
  * hang visible and bounded; it does not kill the hung work.
692
821
  */
693
- async #withHookBudget<T>(pipName: string, hook: DotLifecycleHook, run: () => Promise<T> | T): Promise<T> {
822
+ async #withHookBudget<T>(pluginName: string, hook: DotLifecycleHook, run: () => Promise<T> | T): Promise<T> {
694
823
  const budget = this.#hookTimeoutMs;
695
824
  if (budget === undefined) return run();
696
825
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -700,9 +829,9 @@ export class DotAppImpl {
700
829
  new DotLifecycleError({
701
830
  code: DotLifecycleErrorCode.HookTimeout,
702
831
  phase: hook,
703
- pip: pipName,
832
+ plugin: pluginName,
704
833
  message:
705
- `${hook} hook of pip "${pipName}" exceeded the ${budget.toString()}ms hookTimeoutMs watchdog. ` +
834
+ `${hook} hook of plugin "${pluginName}" exceeded the ${budget.toString()}ms hookTimeoutMs watchdog. ` +
706
835
  `The kernel treats the hook as failed and applies its normal rollback/aggregation rules, but ` +
707
836
  `cannot cancel the hook's promise — find the hang (a missing await? a connection that never ` +
708
837
  `settles?) or raise the budget in defineApp(name, { hookTimeoutMs }).`,
@@ -725,11 +854,11 @@ export class DotAppImpl {
725
854
  * partial failure — stays unindented.
726
855
  */
727
856
  async #runBootInner(phaseLogger: Logger): Promise<void> {
728
- const bootedRecords: PipRecord[] = [];
857
+ const bootedRecords: PluginRecord[] = [];
729
858
 
730
859
  /** Shared failure path: mark diagnostics, roll back, throw. */
731
860
  const fail = async (args: {
732
- record: PipRecord;
861
+ record: PluginRecord;
733
862
  code: (typeof DotLifecycleErrorCode)[keyof typeof DotLifecycleErrorCode];
734
863
  message: string;
735
864
  remediation: string;
@@ -737,30 +866,30 @@ export class DotAppImpl {
737
866
  durationMs: number;
738
867
  cause?: unknown;
739
868
  /** Records to dispose (reverse order applied here). */
740
- rollback: readonly PipRecord[];
869
+ rollback: readonly PluginRecord[];
741
870
  }): Promise<never> => {
742
871
  const issue = makeIssue({
743
872
  code: args.code,
744
- pip: args.record.pip.name,
873
+ plugin: args.record.plugin.name,
745
874
  message: args.message,
746
875
  remediation: args.remediation,
747
876
  docsAnchor: args.docsAnchor,
748
877
  });
749
878
  args.record.issues.push(issue);
750
879
  args.record.lifecycleDiagnostics.push({
751
- pip: args.record.pip.name,
880
+ plugin: args.record.plugin.name,
752
881
  hook: 'boot',
753
882
  state: 'failed',
754
883
  order: args.record.order,
755
884
  durationMs: args.durationMs,
756
885
  issues: [issue],
757
886
  });
758
- this.#emitHook('boot', args.record.pip.name, args.record.order, 'failed', {
887
+ this.#emitHook('boot', args.record.plugin.name, args.record.order, 'failed', {
759
888
  durationMs: args.durationMs,
760
889
  error: args.cause ?? new Error(args.message),
761
890
  });
762
- phaseLogger.error('boot: FAILED — rolling back already-booted pips', {
763
- 'dot.pip.name': args.record.pip.name,
891
+ phaseLogger.error('boot: FAILED — rolling back already-booted plugins', {
892
+ 'dot.plugin.name': args.record.plugin.name,
764
893
  'dot.app.rollback.count': args.rollback.length,
765
894
  });
766
895
  const disposeFailures = await this.#runDisposeForRecords(reverseRecords(args.rollback), phaseLogger);
@@ -769,19 +898,19 @@ export class DotAppImpl {
769
898
  throw new DotLifecycleError({
770
899
  code: args.code,
771
900
  phase: 'boot',
772
- pip: args.record.pip.name,
901
+ plugin: args.record.plugin.name,
773
902
  message: args.message,
774
903
  cause: args.cause,
775
904
  failures: disposeFailures.length > 0 ? disposeFailures : undefined,
776
905
  });
777
906
  };
778
907
 
779
- for (const pip of this.#ordered) {
780
- const record = this.#records.get(pip.name)!;
781
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', pip.name);
908
+ for (const plugin of this.#ordered) {
909
+ const record = this.#records.get(plugin.name)!;
910
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
782
911
  const started = performance.now();
783
912
 
784
- // Resolve needs BEFORE invoking the hook — a pip with needs but no
913
+ // Resolve needs BEFORE invoking the hook — a plugin with needs but no
785
914
  // boot hook still fails fast when its wiring is unsatisfied.
786
915
  let ctx: Record<string, unknown>;
787
916
  try {
@@ -793,40 +922,40 @@ export class DotAppImpl {
793
922
  code: DotLifecycleErrorCode.UnsatisfiedNeed,
794
923
  message,
795
924
  remediation:
796
- `Add a provider for the missing service with .use() before "${pip.name}", or rename an ` +
797
- `existing provider's keys to match. If a later pip provides it, reorder — services flow ` +
798
- `strictly forward. Mutual needs between two pips are a cycle: merge them or extract the ` +
799
- `shared piece into a third pip both consume.`,
925
+ `Add a provider for the missing service with .use() before "${plugin.name}", or rename an ` +
926
+ `existing provider's keys to match. If a later plugin provides it, reorder — services flow ` +
927
+ `strictly forward. Mutual needs between two plugins are a cycle: merge them or extract the ` +
928
+ `shared piece into a third plugin both consume.`,
800
929
  docsAnchor: 'unsatisfied-need',
801
930
  durationMs: performance.now() - started,
802
931
  rollback: bootedRecords,
803
932
  });
804
933
  }
805
934
 
806
- if (!pip.hooks.boot) {
935
+ if (!plugin.hooks.boot) {
807
936
  record.booted = true;
808
937
  bootedRecords.push(record);
809
- pipLogger.debug('boot: skipped (no hook)');
938
+ pluginLogger.debug('boot: skipped (no hook)');
810
939
  continue;
811
940
  }
812
941
  record.hooks.add('boot');
813
942
 
814
- this.#emitHook('boot', pip.name, record.order, 'starting');
943
+ this.#emitHook('boot', plugin.name, record.order, 'starting');
815
944
  let result: ServiceRecord | void;
816
945
  try {
817
- result = await this.#withHookBudget(pip.name, 'boot', () =>
818
- withPipHookSpan(
946
+ result = await this.#withHookBudget(plugin.name, 'boot', () =>
947
+ withPluginHookSpan(
819
948
  {
820
949
  appName: this.#appName,
821
- pipName: pip.name,
822
- pipVersion: pip.version,
950
+ pluginName: plugin.name,
951
+ pluginVersion: plugin.version,
823
952
  hook: 'boot',
824
953
  order: record.order,
825
- logger: pipLogger,
954
+ logger: pluginLogger,
826
955
  },
827
956
  // Erasure boundary: hooks are stored as `(ctx: never) => ...`;
828
957
  // the kernel is the one caller allowed to cross it.
829
- () => pip.hooks.boot!(ctx as never),
958
+ () => plugin.hooks.boot!(ctx as never),
830
959
  ),
831
960
  );
832
961
  } catch (error) {
@@ -836,10 +965,10 @@ export class DotAppImpl {
836
965
  code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.BootFailed,
837
966
  message: timedOut
838
967
  ? error.message
839
- : `boot hook threw for pip "${pip.name}": ${stringifyError(error)}`,
968
+ : `boot hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
840
969
  remediation: timedOut
841
- ? `Find the hang in the boot() hook of "${pip.name}" — a missing await or a connection that never settles — or raise defineApp's hookTimeoutMs.`
842
- : `Fix the error in the boot() hook of "${pip.name}". If boot opens partial resources before throwing, clean them up locally — DOT only disposes pips whose boot completed.`,
970
+ ? `Find the hang in the boot() hook of "${plugin.name}" — a missing await or a connection that never settles — or raise defineApp's hookTimeoutMs.`
971
+ : `Fix the error in the boot() hook of "${plugin.name}". If boot opens partial resources before throwing, clean them up locally — DOT only disposes plugins whose boot completed.`,
843
972
  docsAnchor: timedOut ? 'hook-timeout' : 'boot-failed',
844
973
  durationMs: performance.now() - started,
845
974
  cause: error,
@@ -852,18 +981,18 @@ export class DotAppImpl {
852
981
  record.booted = true;
853
982
 
854
983
  for (const [localKey, value] of Object.entries(publishedServices)) {
855
- const wireKey = pip.renames[localKey] ?? localKey;
984
+ const wireKey = plugin.renames[localKey] ?? localKey;
856
985
  if (localKey.startsWith('$') || wireKey.startsWith('$')) {
857
- // The pip() constraint bans this at compile time; erased pips can
858
- // still reach here. A `$` publish would shadow $app/$pip/$config
859
- // in the pip's own post-boot hook contexts.
986
+ // The plugin() constraint bans this at compile time; erased plugins can
987
+ // still reach here. A `$` publish would shadow $app/$plugin/$config
988
+ // in the plugin's own post-boot hook contexts.
860
989
  return fail({
861
990
  record,
862
991
  code: DotLifecycleErrorCode.ReservedServiceKey,
863
992
  message:
864
- `Pip "${pip.name}" publishes service "${wireKey}" — the "$" prefix is reserved ` +
865
- `for kernel context keys ($app, $pip, $config).`,
866
- remediation: `Rename the "${wireKey}" key returned from the boot() hook of "${pip.name}" — "$"-prefixed keys would shadow the kernel context.`,
993
+ `Plugin "${plugin.name}" publishes service "${wireKey}" — the "$" prefix is reserved ` +
994
+ `for kernel context keys ($app, $plugin, $config).`,
995
+ remediation: `Rename the "${wireKey}" key returned from the boot() hook of "${plugin.name}" — "$"-prefixed keys would shadow the kernel context.`,
867
996
  docsAnchor: 'reserved-service-key',
868
997
  durationMs: performance.now() - started,
869
998
  rollback: [...bootedRecords, record],
@@ -871,19 +1000,19 @@ export class DotAppImpl {
871
1000
  }
872
1001
  if (this.#serviceMap.has(wireKey)) {
873
1002
  const owner = this.#providerByWireKey.get(wireKey) ?? 'unknown';
874
- // The current pip HAS booted — include it in the rollback.
1003
+ // The current plugin HAS booted — include it in the rollback.
875
1004
  return fail({
876
1005
  record,
877
1006
  code: DotLifecycleErrorCode.ServiceCollision,
878
- message: `Pip "${pip.name}" publishes service "${wireKey}" which pip "${owner}" already provides.`,
879
- remediation: `Mount one of the two with rename(pip, { '${wireKey}': '<newKey>' }) to keep both instances, or remove the duplicate provider.`,
1007
+ message: `Plugin "${plugin.name}" publishes service "${wireKey}" which plugin "${owner}" already provides.`,
1008
+ remediation: `Mount one of the two with rename(plugin, { '${wireKey}': '<newKey>' }) to keep both instances, or remove the duplicate provider.`,
880
1009
  docsAnchor: 'service-collision',
881
1010
  durationMs: performance.now() - started,
882
1011
  rollback: [...bootedRecords, record],
883
1012
  });
884
1013
  }
885
1014
  this.#serviceMap.set(wireKey, value);
886
- this.#providerByWireKey.set(wireKey, pip.name);
1015
+ this.#providerByWireKey.set(wireKey, plugin.name);
887
1016
  this.#aggregatedServices[wireKey] = value;
888
1017
  record.provides.add(wireKey);
889
1018
  }
@@ -891,23 +1020,23 @@ export class DotAppImpl {
891
1020
 
892
1021
  const durationMs = performance.now() - started;
893
1022
  record.lifecycleDiagnostics.push({
894
- pip: pip.name,
1023
+ plugin: plugin.name,
895
1024
  hook: 'boot',
896
1025
  state: 'booted',
897
1026
  order: record.order,
898
1027
  durationMs,
899
1028
  issues: [],
900
1029
  });
901
- this.#emitHook('boot', pip.name, record.order, 'completed', { durationMs });
902
- pipLogger.debug('boot: done', {
903
- 'dot.pip.duration.ms': durationMs,
904
- 'dot.pip.services.published': Object.keys(publishedServices).length,
1030
+ this.#emitHook('boot', plugin.name, record.order, 'completed', { durationMs });
1031
+ pluginLogger.debug('boot: done', {
1032
+ 'dot.plugin.duration.ms': durationMs,
1033
+ 'dot.plugin.services.published': Object.keys(publishedServices).length,
905
1034
  });
906
1035
  }
907
1036
 
908
1037
  this.#state = 'booted';
909
1038
  this.#manifest = this.#buildManifest();
910
- phaseLogger.debug('boot: complete', { 'dot.app.pip.count': this.#ordered.length });
1039
+ phaseLogger.debug('boot: complete', { 'dot.app.plugin.count': this.#ordered.length });
911
1040
  }
912
1041
 
913
1042
  /** Public start(). Boots first if needed. Idempotent + concurrent-safe. */
@@ -933,14 +1062,14 @@ export class DotAppImpl {
933
1062
  }
934
1063
  // From booted or stopped, run start hooks.
935
1064
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'start');
936
- phaseLogger.debug('start: starting', { 'dot.app.pip.count': this.#ordered.length });
1065
+ phaseLogger.debug('start: starting', { 'dot.app.plugin.count': this.#ordered.length });
937
1066
 
938
1067
  return this.#withPhaseEmitAsync('start', () =>
939
1068
  withPhaseSpan(
940
1069
  {
941
1070
  appName: this.#appName,
942
1071
  phase: 'start',
943
- pipCount: this.#ordered.length,
1072
+ pluginCount: this.#ordered.length,
944
1073
  logger: phaseLogger,
945
1074
  },
946
1075
  () => this.#runStartInner(phaseLogger),
@@ -953,12 +1082,12 @@ export class DotAppImpl {
953
1082
  * stays thin and the rollback-cascade error path stays readable.
954
1083
  */
955
1084
  async #runStartInner(phaseLogger: Logger): Promise<void> {
956
- const startedRecords: PipRecord[] = [];
957
- for (const pip of this.#ordered) {
958
- const record = this.#records.get(pip.name)!;
959
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', pip.name);
960
- if (!pip.hooks.start) {
961
- pipLogger.debug('start: skipped (no hook)');
1085
+ const startedRecords: PluginRecord[] = [];
1086
+ for (const plugin of this.#ordered) {
1087
+ const record = this.#records.get(plugin.name)!;
1088
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
1089
+ if (!plugin.hooks.start) {
1090
+ pluginLogger.debug('start: skipped (no hook)');
962
1091
  continue;
963
1092
  }
964
1093
  record.hooks.add('start');
@@ -966,19 +1095,19 @@ export class DotAppImpl {
966
1095
  const ctx = this.#buildHookCtx(record, 'start', true);
967
1096
 
968
1097
  const startedAt = performance.now();
969
- this.#emitHook('start', pip.name, record.order, 'starting');
1098
+ this.#emitHook('start', plugin.name, record.order, 'starting');
970
1099
  try {
971
- await this.#withHookBudget(pip.name, 'start', () =>
972
- withPipHookSpan(
1100
+ await this.#withHookBudget(plugin.name, 'start', () =>
1101
+ withPluginHookSpan(
973
1102
  {
974
1103
  appName: this.#appName,
975
- pipName: pip.name,
976
- pipVersion: pip.version,
1104
+ pluginName: plugin.name,
1105
+ pluginVersion: plugin.version,
977
1106
  hook: 'start',
978
1107
  order: record.order,
979
- logger: pipLogger,
1108
+ logger: pluginLogger,
980
1109
  },
981
- () => pip.hooks.start!(ctx as never),
1110
+ () => plugin.hooks.start!(ctx as never),
982
1111
  ),
983
1112
  );
984
1113
  } catch (error) {
@@ -986,27 +1115,27 @@ export class DotAppImpl {
986
1115
  const timedOut = error instanceof DotLifecycleError && error.code === DotLifecycleErrorCode.HookTimeout;
987
1116
  const issue = makeIssue({
988
1117
  code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.StartFailed,
989
- pip: pip.name,
1118
+ plugin: plugin.name,
990
1119
  message: timedOut
991
1120
  ? error.message
992
- : `start hook threw for pip "${pip.name}": ${stringifyError(error)}`,
1121
+ : `start hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
993
1122
  remediation: timedOut
994
- ? `Find the hang in the start() hook of "${pip.name}" — or raise defineApp's hookTimeoutMs. DOT rolls back as for any start failure.`
995
- : `Fix the error in the start() hook of "${pip.name}". DOT will stop all already-started pips and dispose all booted pips in reverse order.`,
1123
+ ? `Find the hang in the start() hook of "${plugin.name}" — or raise defineApp's hookTimeoutMs. DOT rolls back as for any start failure.`
1124
+ : `Fix the error in the start() hook of "${plugin.name}". DOT will stop all already-started plugins and dispose all booted plugins in reverse order.`,
996
1125
  docsAnchor: timedOut ? 'hook-timeout' : 'start-failed',
997
1126
  });
998
1127
  record.issues.push(issue);
999
1128
  record.lifecycleDiagnostics.push({
1000
- pip: pip.name,
1129
+ plugin: plugin.name,
1001
1130
  hook: 'start',
1002
1131
  state: 'failed',
1003
1132
  order: record.order,
1004
1133
  durationMs,
1005
1134
  issues: [issue],
1006
1135
  });
1007
- this.#emitHook('start', pip.name, record.order, 'failed', { durationMs, error });
1136
+ this.#emitHook('start', plugin.name, record.order, 'failed', { durationMs, error });
1008
1137
 
1009
- pipLogger.error('start: FAILED — rolling back', {
1138
+ pluginLogger.error('start: FAILED — rolling back', {
1010
1139
  'dot.app.rollback.started.count': startedRecords.length,
1011
1140
  });
1012
1141
  const stopFailures = await this.#runStopForRecords(reverseRecords(startedRecords), phaseLogger);
@@ -1018,7 +1147,7 @@ export class DotAppImpl {
1018
1147
  throw new DotLifecycleError({
1019
1148
  code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.StartFailed,
1020
1149
  phase: 'start',
1021
- pip: pip.name,
1150
+ plugin: plugin.name,
1022
1151
  message: issue.message,
1023
1152
  cause: error,
1024
1153
  failures: failures.length > 0 ? failures : undefined,
@@ -1029,20 +1158,20 @@ export class DotAppImpl {
1029
1158
  startedRecords.push(record);
1030
1159
  const durationMs = performance.now() - startedAt;
1031
1160
  record.lifecycleDiagnostics.push({
1032
- pip: pip.name,
1161
+ plugin: plugin.name,
1033
1162
  hook: 'start',
1034
1163
  state: 'started',
1035
1164
  order: record.order,
1036
1165
  durationMs,
1037
1166
  issues: [],
1038
1167
  });
1039
- this.#emitHook('start', pip.name, record.order, 'completed', { durationMs });
1040
- pipLogger.debug('start: done', { 'dot.pip.duration.ms': durationMs });
1168
+ this.#emitHook('start', plugin.name, record.order, 'completed', { durationMs });
1169
+ pluginLogger.debug('start: done', { 'dot.plugin.duration.ms': durationMs });
1041
1170
  }
1042
1171
 
1043
1172
  this.#state = 'started';
1044
1173
  this.#manifest = this.#buildManifest();
1045
- phaseLogger.debug('start: complete', { 'dot.app.pip.count': this.#ordered.length });
1174
+ phaseLogger.debug('start: complete', { 'dot.app.plugin.count': this.#ordered.length });
1046
1175
  }
1047
1176
 
1048
1177
  /** Public stop(). Idempotent + concurrent-safe. */
@@ -1071,14 +1200,14 @@ export class DotAppImpl {
1071
1200
 
1072
1201
  async #runStop(): Promise<void> {
1073
1202
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'stop');
1074
- phaseLogger.debug('stop: starting', { 'dot.app.pip.count': this.#ordered.length });
1203
+ phaseLogger.debug('stop: starting', { 'dot.app.plugin.count': this.#ordered.length });
1075
1204
 
1076
1205
  return this.#withPhaseEmitAsync('stop', () =>
1077
1206
  withPhaseSpan(
1078
1207
  {
1079
1208
  appName: this.#appName,
1080
1209
  phase: 'stop',
1081
- pipCount: this.#ordered.length,
1210
+ pluginCount: this.#ordered.length,
1082
1211
  logger: phaseLogger,
1083
1212
  },
1084
1213
  async () => {
@@ -1091,73 +1220,73 @@ export class DotAppImpl {
1091
1220
  throw new DotLifecycleError({
1092
1221
  code: DotLifecycleErrorCode.StopFailed,
1093
1222
  phase: 'stop',
1094
- message: `${failures.length} pip(s) failed during stop`,
1223
+ message: `${failures.length} plugin(s) failed during stop`,
1095
1224
  failures,
1096
1225
  });
1097
1226
  }
1098
- phaseLogger.debug('stop: complete', { 'dot.app.pip.count': this.#ordered.length });
1227
+ phaseLogger.debug('stop: complete', { 'dot.app.plugin.count': this.#ordered.length });
1099
1228
  },
1100
1229
  ),
1101
1230
  );
1102
1231
  }
1103
1232
 
1104
- async #runStopForRecords(records: readonly PipRecord[], phaseLogger: Logger): Promise<DotLifecyclePipFailure[]> {
1105
- const failures: DotLifecyclePipFailure[] = [];
1233
+ async #runStopForRecords(records: readonly PluginRecord[], phaseLogger: Logger): Promise<DotLifecyclePluginFailure[]> {
1234
+ const failures: DotLifecyclePluginFailure[] = [];
1106
1235
  for (const record of records) {
1107
- if (!record.pip.hooks.stop) continue;
1108
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', record.pip.name);
1236
+ if (!record.plugin.hooks.stop) continue;
1237
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', record.plugin.name);
1109
1238
  record.hooks.add('stop');
1110
1239
  const ctx = this.#buildHookCtx(record, 'stop', true);
1111
1240
  const startedAt = performance.now();
1112
- this.#emitHook('stop', record.pip.name, record.order, 'starting');
1241
+ this.#emitHook('stop', record.plugin.name, record.order, 'starting');
1113
1242
  try {
1114
- await this.#withHookBudget(record.pip.name, 'stop', () =>
1115
- withPipHookSpan(
1243
+ await this.#withHookBudget(record.plugin.name, 'stop', () =>
1244
+ withPluginHookSpan(
1116
1245
  {
1117
1246
  appName: this.#appName,
1118
- pipName: record.pip.name,
1119
- pipVersion: record.pip.version,
1247
+ pluginName: record.plugin.name,
1248
+ pluginVersion: record.plugin.version,
1120
1249
  hook: 'stop',
1121
1250
  order: record.order,
1122
- logger: pipLogger,
1251
+ logger: pluginLogger,
1123
1252
  },
1124
- () => record.pip.hooks.stop!(ctx as never),
1253
+ () => record.plugin.hooks.stop!(ctx as never),
1125
1254
  ),
1126
1255
  );
1127
1256
  record.started = false;
1128
1257
  const durationMs = performance.now() - startedAt;
1129
1258
  record.lifecycleDiagnostics.push({
1130
- pip: record.pip.name,
1259
+ plugin: record.plugin.name,
1131
1260
  hook: 'stop',
1132
1261
  state: 'stopped',
1133
1262
  order: record.order,
1134
1263
  durationMs,
1135
1264
  issues: [],
1136
1265
  });
1137
- this.#emitHook('stop', record.pip.name, record.order, 'completed', { durationMs });
1138
- pipLogger.debug('stop: done', { 'dot.pip.duration.ms': durationMs });
1266
+ this.#emitHook('stop', record.plugin.name, record.order, 'completed', { durationMs });
1267
+ pluginLogger.debug('stop: done', { 'dot.plugin.duration.ms': durationMs });
1139
1268
  } catch (error) {
1140
1269
  const durationMs = performance.now() - startedAt;
1141
1270
  const issue = makeIssue({
1142
1271
  code: DotLifecycleErrorCode.StopFailed,
1143
- pip: record.pip.name,
1144
- message: `stop hook threw for pip "${record.pip.name}": ${stringifyError(error)}`,
1145
- remediation: `Fix the error in the stop() hook of "${record.pip.name}". Stop continues through individual failures and reports an aggregate error.`,
1272
+ plugin: record.plugin.name,
1273
+ message: `stop hook threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
1274
+ remediation: `Fix the error in the stop() hook of "${record.plugin.name}". Stop continues through individual failures and reports an aggregate error.`,
1146
1275
  docsAnchor: 'stop-failed',
1147
1276
  });
1148
1277
  record.issues.push(issue);
1149
1278
  record.lifecycleDiagnostics.push({
1150
- pip: record.pip.name,
1279
+ plugin: record.plugin.name,
1151
1280
  hook: 'stop',
1152
1281
  state: 'failed',
1153
1282
  order: record.order,
1154
1283
  durationMs,
1155
1284
  issues: [issue],
1156
1285
  });
1157
- this.#emitHook('stop', record.pip.name, record.order, 'failed', { durationMs, error });
1158
- failures.push({ pip: record.pip.name, phase: 'stop', error });
1159
- pipLogger.error('stop: failed (continuing)', {
1160
- 'dot.pip.error.message': stringifyError(error),
1286
+ this.#emitHook('stop', record.plugin.name, record.order, 'failed', { durationMs, error });
1287
+ failures.push({ plugin: record.plugin.name, phase: 'stop', error });
1288
+ pluginLogger.error('stop: failed (continuing)', {
1289
+ 'dot.plugin.error.message': stringifyError(error),
1161
1290
  });
1162
1291
  }
1163
1292
  }
@@ -1181,14 +1310,14 @@ export class DotAppImpl {
1181
1310
 
1182
1311
  async #runDispose(): Promise<void> {
1183
1312
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'dispose');
1184
- phaseLogger.debug('dispose: starting', { 'dot.app.pip.count': this.#ordered.length });
1313
+ phaseLogger.debug('dispose: starting', { 'dot.app.plugin.count': this.#ordered.length });
1185
1314
 
1186
1315
  return this.#withPhaseEmitAsync('dispose', () =>
1187
1316
  withPhaseSpan(
1188
1317
  {
1189
1318
  appName: this.#appName,
1190
1319
  phase: 'dispose',
1191
- pipCount: this.#ordered.length,
1320
+ pluginCount: this.#ordered.length,
1192
1321
  logger: phaseLogger,
1193
1322
  },
1194
1323
  () => this.#runDisposeInner(phaseLogger),
@@ -1221,7 +1350,7 @@ export class DotAppImpl {
1221
1350
  throw new DotLifecycleError({
1222
1351
  code: DotLifecycleErrorCode.DisposeFailed,
1223
1352
  phase: 'dispose',
1224
- message: `${failures.length} pip(s) failed during stop+dispose cascade`,
1353
+ message: `${failures.length} plugin(s) failed during stop+dispose cascade`,
1225
1354
  failures,
1226
1355
  });
1227
1356
  }
@@ -1229,7 +1358,7 @@ export class DotAppImpl {
1229
1358
  return;
1230
1359
  }
1231
1360
 
1232
- // From booted/stopped/configured/defined: only dispose booted pips.
1361
+ // From booted/stopped/configured/defined: only dispose booted plugins.
1233
1362
  if (this.#state === 'failed') {
1234
1363
  // Already cleaned up at the failure site — mark disposed.
1235
1364
  this.#state = 'disposed';
@@ -1247,15 +1376,15 @@ export class DotAppImpl {
1247
1376
  throw new DotLifecycleError({
1248
1377
  code: DotLifecycleErrorCode.DisposeFailed,
1249
1378
  phase: 'dispose',
1250
- message: `${failures.length} pip(s) failed during dispose`,
1379
+ message: `${failures.length} plugin(s) failed during dispose`,
1251
1380
  failures,
1252
1381
  });
1253
1382
  }
1254
- phaseLogger.debug('dispose: complete', { 'dot.app.pip.count': this.#ordered.length });
1383
+ phaseLogger.debug('dispose: complete', { 'dot.app.plugin.count': this.#ordered.length });
1255
1384
  }
1256
1385
 
1257
- async #runDisposeForRecords(records: readonly PipRecord[], phaseLogger: Logger): Promise<DotLifecyclePipFailure[]> {
1258
- const failures: DotLifecyclePipFailure[] = [];
1386
+ async #runDisposeForRecords(records: readonly PluginRecord[], phaseLogger: Logger): Promise<DotLifecyclePluginFailure[]> {
1387
+ const failures: DotLifecyclePluginFailure[] = [];
1259
1388
 
1260
1389
  // A lazy handle can be published under several keys — a `service.lazy`
1261
1390
  // consumer republishing its provider's handle passes it through by
@@ -1263,8 +1392,8 @@ export class DotAppImpl {
1263
1392
  // (declaration order): dispose runs in reverse, so cleaning the handle
1264
1393
  // with a republisher would kill it before the original provider's own
1265
1394
  // dispose hook gets to use it. `records` arrives reversed, so plain
1266
- // overwrite leaves the earliest-declared pip as each handle's owner.
1267
- const ownerOf = new Map<Lazy<unknown>, PipRecord>();
1395
+ // overwrite leaves the earliest-declared plugin as each handle's owner.
1396
+ const ownerOf = new Map<Lazy<unknown>, PluginRecord>();
1268
1397
  for (const record of records) {
1269
1398
  for (const value of Object.values(record.publishedServices)) {
1270
1399
  if (isLazy(value)) ownerOf.set(value, record);
@@ -1281,16 +1410,16 @@ export class DotAppImpl {
1281
1410
  return true;
1282
1411
  },
1283
1412
  );
1284
- const hasHook = record.pip.hooks.dispose !== undefined;
1413
+ const hasHook = record.plugin.hooks.dispose !== undefined;
1285
1414
  if (!hasHook && lazyPublishes.length === 0) {
1286
1415
  record.booted = false;
1287
1416
  continue;
1288
1417
  }
1289
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', record.pip.name);
1418
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', record.plugin.name);
1290
1419
  if (hasHook) {
1291
- await this.#runDisposeHook(record, pipLogger, failures);
1420
+ await this.#runDisposeHook(record, pluginLogger, failures);
1292
1421
  }
1293
- // Auto-dispose lazy service handles AFTER the pip's own dispose hook
1422
+ // Auto-dispose lazy service handles AFTER the plugin's own dispose hook
1294
1423
  // (the hook may still use them). Never-initialized handles no-op.
1295
1424
  for (const [serviceKey, handle] of lazyPublishes) {
1296
1425
  try {
@@ -1298,16 +1427,16 @@ export class DotAppImpl {
1298
1427
  } catch (error) {
1299
1428
  const issue = makeIssue({
1300
1429
  code: DotLifecycleErrorCode.DisposeFailed,
1301
- pip: record.pip.name,
1302
- message: `lazy service "${serviceKey}" cleanup threw for pip "${record.pip.name}": ${stringifyError(error)}`,
1430
+ plugin: record.plugin.name,
1431
+ message: `lazy service "${serviceKey}" cleanup threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
1303
1432
  remediation: `Fix the error in the dispose callback passed to lazy(...) for service "${serviceKey}". Dispose continues through individual failures and reports an aggregate error.`,
1304
1433
  docsAnchor: 'dispose-failed',
1305
1434
  });
1306
1435
  record.issues.push(issue);
1307
- failures.push({ pip: record.pip.name, phase: 'dispose', error });
1308
- pipLogger.error('dispose: lazy service cleanup failed (continuing)', {
1309
- 'dot.pip.service.key': serviceKey,
1310
- 'dot.pip.error.message': stringifyError(error),
1436
+ failures.push({ plugin: record.plugin.name, phase: 'dispose', error });
1437
+ pluginLogger.error('dispose: lazy service cleanup failed (continuing)', {
1438
+ 'dot.plugin.service.key': serviceKey,
1439
+ 'dot.plugin.error.message': stringifyError(error),
1311
1440
  });
1312
1441
  }
1313
1442
  }
@@ -1318,60 +1447,60 @@ export class DotAppImpl {
1318
1447
  return failures;
1319
1448
  }
1320
1449
 
1321
- /** Run a single pip's dispose hook with spans/events/diagnostics. */
1322
- async #runDisposeHook(record: PipRecord, pipLogger: Logger, failures: DotLifecyclePipFailure[]): Promise<void> {
1450
+ /** Run a single plugin's dispose hook with spans/events/diagnostics. */
1451
+ async #runDisposeHook(record: PluginRecord, pluginLogger: Logger, failures: DotLifecyclePluginFailure[]): Promise<void> {
1323
1452
  record.hooks.add('dispose');
1324
1453
  const ctx = this.#buildHookCtx(record, 'dispose', true);
1325
1454
  const startedAt = performance.now();
1326
- this.#emitHook('dispose', record.pip.name, record.order, 'starting');
1455
+ this.#emitHook('dispose', record.plugin.name, record.order, 'starting');
1327
1456
  try {
1328
- await this.#withHookBudget(record.pip.name, 'dispose', () =>
1329
- withPipHookSpan(
1457
+ await this.#withHookBudget(record.plugin.name, 'dispose', () =>
1458
+ withPluginHookSpan(
1330
1459
  {
1331
1460
  appName: this.#appName,
1332
- pipName: record.pip.name,
1333
- pipVersion: record.pip.version,
1461
+ pluginName: record.plugin.name,
1462
+ pluginVersion: record.plugin.version,
1334
1463
  hook: 'dispose',
1335
1464
  order: record.order,
1336
- logger: pipLogger,
1465
+ logger: pluginLogger,
1337
1466
  },
1338
- () => record.pip.hooks.dispose!(ctx as never),
1467
+ () => record.plugin.hooks.dispose!(ctx as never),
1339
1468
  ),
1340
1469
  );
1341
1470
  record.booted = false;
1342
1471
  const durationMs = performance.now() - startedAt;
1343
1472
  record.lifecycleDiagnostics.push({
1344
- pip: record.pip.name,
1473
+ plugin: record.plugin.name,
1345
1474
  hook: 'dispose',
1346
1475
  state: 'disposed',
1347
1476
  order: record.order,
1348
1477
  durationMs,
1349
1478
  issues: [],
1350
1479
  });
1351
- this.#emitHook('dispose', record.pip.name, record.order, 'completed', { durationMs });
1352
- pipLogger.debug('dispose: done', { 'dot.pip.duration.ms': durationMs });
1480
+ this.#emitHook('dispose', record.plugin.name, record.order, 'completed', { durationMs });
1481
+ pluginLogger.debug('dispose: done', { 'dot.plugin.duration.ms': durationMs });
1353
1482
  } catch (error) {
1354
1483
  const durationMs = performance.now() - startedAt;
1355
1484
  const issue = makeIssue({
1356
1485
  code: DotLifecycleErrorCode.DisposeFailed,
1357
- pip: record.pip.name,
1358
- message: `dispose hook threw for pip "${record.pip.name}": ${stringifyError(error)}`,
1359
- remediation: `Fix the error in the dispose() hook of "${record.pip.name}". Dispose continues through individual failures and reports an aggregate error.`,
1486
+ plugin: record.plugin.name,
1487
+ message: `dispose hook threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
1488
+ remediation: `Fix the error in the dispose() hook of "${record.plugin.name}". Dispose continues through individual failures and reports an aggregate error.`,
1360
1489
  docsAnchor: 'dispose-failed',
1361
1490
  });
1362
1491
  record.issues.push(issue);
1363
1492
  record.lifecycleDiagnostics.push({
1364
- pip: record.pip.name,
1493
+ plugin: record.plugin.name,
1365
1494
  hook: 'dispose',
1366
1495
  state: 'failed',
1367
1496
  order: record.order,
1368
1497
  durationMs,
1369
1498
  issues: [issue],
1370
1499
  });
1371
- this.#emitHook('dispose', record.pip.name, record.order, 'failed', { durationMs, error });
1372
- failures.push({ pip: record.pip.name, phase: 'dispose', error });
1373
- pipLogger.error('dispose: failed (continuing)', {
1374
- 'dot.pip.error.message': stringifyError(error),
1500
+ this.#emitHook('dispose', record.plugin.name, record.order, 'failed', { durationMs, error });
1501
+ failures.push({ plugin: record.plugin.name, phase: 'dispose', error });
1502
+ pluginLogger.error('dispose: failed (continuing)', {
1503
+ 'dot.plugin.error.message': stringifyError(error),
1375
1504
  });
1376
1505
  }
1377
1506
  }
@@ -1388,67 +1517,72 @@ export class DotAppImpl {
1388
1517
  }
1389
1518
 
1390
1519
  #buildManifest(): DotAppManifest {
1391
- const pips: PipManifest[] = [];
1392
- const routes: RouteManifest[] = [];
1520
+ const plugins: PluginManifest[] = [];
1521
+ const actions: ActionManifest[] = [];
1393
1522
  const services: ServiceManifest[] = [];
1394
1523
  const lifecycle: LifecycleManifest[] = [];
1395
- // Edges are observed at boot time: consumer pip → the pip whose
1396
- // published wire key satisfied its need. Before boot, the per-pip
1524
+ const projections: ProjectionManifest[] = [];
1525
+ // Edges are observed at boot time: consumer plugin the plugin whose
1526
+ // published wire key satisfied its need. Before boot, the per-plugin
1397
1527
  // `dependencies` array (wire-key strings) is the declarative view.
1398
1528
  const dependencies: DependencyEdge[] = this.#wiringEdges.map(e => ({ ...e }));
1399
1529
 
1400
- for (const pip of this.#ordered) {
1401
- const record = this.#records.get(pip.name)!;
1402
- const needWireKeys = Object.entries(pip.needs).map(([alias, witness]) => wireKeyOf(witness, alias));
1403
- pips.push({
1404
- name: pip.name,
1405
- version: pip.version,
1530
+ for (const plugin of this.#ordered) {
1531
+ const record = this.#records.get(plugin.name)!;
1532
+ const needWireKeys = Object.entries(plugin.needs).map(([alias, witness]) => wireKeyOf(witness, alias));
1533
+ plugins.push({
1534
+ name: plugin.name,
1535
+ version: plugin.version,
1406
1536
  dependencies: needWireKeys,
1407
1537
  provides: [...record.provides],
1408
1538
  });
1409
- routes.push(...record.routes);
1539
+ actions.push(...record.actions);
1410
1540
  services.push(...record.services);
1541
+ projections.push(...record.projections);
1411
1542
  lifecycle.push({
1412
- pip: pip.name,
1543
+ plugin: plugin.name,
1413
1544
  hooks: [...record.hooks],
1414
1545
  });
1415
1546
  }
1416
1547
 
1417
1548
  return {
1549
+ manifestVersion: 2,
1418
1550
  app: {
1419
1551
  name: this.#appName,
1420
1552
  version: this.#appVersion,
1421
1553
  },
1422
- pips,
1423
- routes,
1554
+ plugins,
1555
+ actions,
1424
1556
  services,
1425
1557
  lifecycle,
1426
1558
  dependencies,
1559
+ projections,
1427
1560
  };
1428
1561
  }
1429
1562
 
1430
1563
  #buildDiagnostics(): DotDiagnosticsSnapshot {
1431
- const pipDiagnostics: PipDiagnostic[] = [];
1432
- const routeDiagnostics: RouteDiagnostic[] = [];
1564
+ const pluginDiagnostics: PluginDiagnostic[] = [];
1565
+ const actionDiagnostics: ActionDiagnostic[] = [];
1433
1566
  const serviceDiagnostics: ServiceDiagnostic[] = [];
1434
1567
  const lifecycleDiagnostics: LifecycleDiagnostic[] = [];
1435
1568
  const issues: DiagnosticIssue[] = [];
1436
1569
 
1437
- for (const pip of this.#ordered) {
1438
- const record = this.#records.get(pip.name)!;
1439
- // Per-pip status: failed if any issue with severity error exists, ok otherwise.
1570
+ for (const plugin of this.#ordered) {
1571
+ const record = this.#records.get(plugin.name)!;
1572
+ // Per-plugin status: failed if any issue with severity error exists, ok otherwise.
1440
1573
  const hasError = record.issues.some(i => i.severity === 'error');
1441
- const status: PipDiagnostic['status'] = hasError ? 'failed' : 'ok';
1442
- pipDiagnostics.push({
1443
- pip: pip.name,
1574
+ const status: PluginDiagnostic['status'] = hasError ? 'failed' : 'ok';
1575
+ pluginDiagnostics.push({
1576
+ plugin: plugin.name,
1444
1577
  status,
1445
1578
  issues: [...record.issues],
1446
1579
  });
1447
1580
 
1448
- for (const route of record.routes) {
1449
- routeDiagnostics.push({
1450
- id: route.id,
1451
- pip: pip.name,
1581
+ for (const action of record.actions) {
1582
+ actionDiagnostics.push({
1583
+ id: action.id,
1584
+ plugin: plugin.name,
1585
+ binding: action.binding,
1452
1586
  status: hasError ? 'failed' : 'ok',
1453
1587
  issues: [],
1454
1588
  });
@@ -1456,7 +1590,7 @@ export class DotAppImpl {
1456
1590
  for (const svc of record.services) {
1457
1591
  serviceDiagnostics.push({
1458
1592
  service: svc.name,
1459
- pip: pip.name,
1593
+ plugin: plugin.name,
1460
1594
  status: hasError
1461
1595
  ? 'failed'
1462
1596
  : record.booted || this.#state === 'defined' || this.#state === 'configured'
@@ -1470,13 +1604,14 @@ export class DotAppImpl {
1470
1604
  }
1471
1605
 
1472
1606
  return {
1607
+ snapshotVersion: 2,
1473
1608
  generatedAt: new Date().toISOString(),
1474
1609
  app: {
1475
1610
  name: this.#appName,
1476
1611
  state: this.#state,
1477
1612
  },
1478
- pips: pipDiagnostics,
1479
- routes: routeDiagnostics,
1613
+ plugins: pluginDiagnostics,
1614
+ actions: actionDiagnostics,
1480
1615
  services: serviceDiagnostics,
1481
1616
  lifecycle: lifecycleDiagnostics,
1482
1617
  issues,
@@ -1503,12 +1638,23 @@ function stringifyError(error: unknown): string {
1503
1638
  }
1504
1639
  }
1505
1640
 
1506
- function reverseRecords(records: readonly PipRecord[]): readonly PipRecord[] {
1641
+ function reverseRecords(records: readonly PluginRecord[]): readonly PluginRecord[] {
1507
1642
  // eslint-disable-next-line unicorn/no-array-reverse -- lib target is ES2022, toReversed is ES2023.
1508
1643
  return [...records].reverse();
1509
1644
  }
1510
1645
 
1511
- /** Re-export `ServiceKind` and `RouteTransport` for the kernel's internal use. */
1646
+ function hasToDotAction(
1647
+ source: AnyPlugin['actions'][number],
1648
+ ): source is { toDotAction(): Omit<ActionManifest, 'plugin'> } {
1649
+ return (
1650
+ typeof source === 'object' &&
1651
+ source !== null &&
1652
+ 'toDotAction' in source &&
1653
+ typeof source.toDotAction === 'function'
1654
+ );
1655
+ }
1656
+
1657
+ /** Re-export `ServiceKind` for the kernel's internal use. */
1512
1658
 
1513
- export { type RouteTransport, type ServiceKind } from '../manifest.js';
1514
- export { type Pip } from '../pip-contract.js';
1659
+ export { type ServiceKind } from '../manifest.js';
1660
+ export { type Plugin } from '../plugin-contract.js';