@arki/dot 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) 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 +11 -1
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +66 -11
  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-openapi.d.ts +38 -0
  24. package/dist/cli/render-openapi.d.ts.map +1 -0
  25. package/dist/cli/render-openapi.js +131 -0
  26. package/dist/cli/render-openapi.js.map +1 -0
  27. package/dist/cli/render-projection.d.ts +29 -0
  28. package/dist/cli/render-projection.d.ts.map +1 -0
  29. package/dist/cli/render-projection.js +202 -0
  30. package/dist/cli/render-projection.js.map +1 -0
  31. package/dist/define-app.d.ts +49 -17
  32. package/dist/define-app.d.ts.map +1 -1
  33. package/dist/define-app.js +16 -9
  34. package/dist/define-app.js.map +1 -1
  35. package/dist/diagnostics.d.ts +19 -15
  36. package/dist/diagnostics.d.ts.map +1 -1
  37. package/dist/diagnostics.js +2 -2
  38. package/dist/index.d.ts +15 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/init-pips.d.ts +45 -0
  43. package/dist/init-pips.d.ts.map +1 -0
  44. package/dist/init-pips.js +49 -0
  45. package/dist/init-pips.js.map +1 -0
  46. package/dist/init-plugins.d.ts +45 -0
  47. package/dist/init-plugins.d.ts.map +1 -0
  48. package/dist/init-plugins.js +49 -0
  49. package/dist/init-plugins.js.map +1 -0
  50. package/dist/kernel/app-instance.d.ts +5 -5
  51. package/dist/kernel/app-instance.d.ts.map +1 -1
  52. package/dist/kernel/app-instance.js +375 -244
  53. package/dist/kernel/app-instance.js.map +1 -1
  54. package/dist/kernel/otel.d.ts +19 -19
  55. package/dist/kernel/otel.d.ts.map +1 -1
  56. package/dist/kernel/otel.js +22 -22
  57. package/dist/kernel/otel.js.map +1 -1
  58. package/dist/lifecycle-observer.d.ts +10 -10
  59. package/dist/lifecycle-observer.d.ts.map +1 -1
  60. package/dist/lifecycle-observer.js +1 -1
  61. package/dist/lifecycle.d.ts +18 -14
  62. package/dist/lifecycle.d.ts.map +1 -1
  63. package/dist/lifecycle.js +13 -9
  64. package/dist/lifecycle.js.map +1 -1
  65. package/dist/manifest.d.ts +70 -29
  66. package/dist/manifest.d.ts.map +1 -1
  67. package/dist/manifest.js +74 -5
  68. package/dist/manifest.js.map +1 -1
  69. package/dist/pip-contract.d.ts +40 -10
  70. package/dist/pip-contract.d.ts.map +1 -1
  71. package/dist/pip-contract.js +1 -25
  72. package/dist/pip-contract.js.map +1 -1
  73. package/dist/pip.d.ts +7 -4
  74. package/dist/pip.d.ts.map +1 -1
  75. package/dist/pip.js +2 -0
  76. package/dist/pip.js.map +1 -1
  77. package/dist/plugin-contract.d.ts +335 -143
  78. package/dist/plugin-contract.d.ts.map +1 -1
  79. package/dist/plugin-contract.js +201 -36
  80. package/dist/plugin-contract.js.map +1 -1
  81. package/dist/plugin.d.ts +18 -15
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/plugin.js +11 -11
  84. package/dist/plugin.js.map +1 -1
  85. package/dist/test-harness.d.ts +31 -31
  86. package/dist/test-harness.d.ts.map +1 -1
  87. package/dist/test-harness.js +29 -28
  88. package/dist/test-harness.js.map +1 -1
  89. package/dist/timeline.d.ts +2 -2
  90. package/dist/timeline.js +5 -5
  91. package/dist/timeline.js.map +1 -1
  92. package/package.json +9 -9
  93. package/src/cli/discover.ts +2 -2
  94. package/src/cli/error-codes.ts +12 -0
  95. package/src/cli/index.ts +93 -15
  96. package/src/cli/new.ts +1 -1
  97. package/src/cli/render-doctor.ts +21 -21
  98. package/src/cli/render-explain.ts +37 -19
  99. package/src/cli/render-graph.ts +12 -12
  100. package/src/cli/render-projection.ts +239 -0
  101. package/src/define-app.ts +85 -22
  102. package/src/diagnostics.ts +19 -15
  103. package/src/index.ts +38 -20
  104. package/src/init-plugins.ts +142 -0
  105. package/src/kernel/app-instance.ts +428 -282
  106. package/src/kernel/otel.ts +27 -27
  107. package/src/lifecycle-observer.ts +10 -10
  108. package/src/lifecycle.ts +19 -15
  109. package/src/manifest.ts +128 -31
  110. package/src/{pip-contract.ts → plugin-contract.ts} +83 -44
  111. package/src/{pip.ts → plugin.ts} +38 -18
  112. package/src/test-harness.ts +52 -51
  113. package/src/timeline.ts +6 -6
  114. package/templates/app-minimal/AGENTS.md.tmpl +9 -9
  115. package/templates/app-minimal/README.md.tmpl +1 -1
  116. package/templates/app-minimal/tests/boot.test.ts.tmpl +1 -1
@@ -7,8 +7,9 @@
7
7
  import { Logger } from '@arki/log';
8
8
  import { createDebugLogger } from '@arki/log/debug';
9
9
  import { DotLifecycleError, DotLifecycleErrorCode } from '../lifecycle.js';
10
- import { isLazy, isLazyWitness, lazyOf } from '../pip-contract.js';
11
- import { withPhaseSpan, withPipHookSpan } from './otel.js';
10
+ import { toJsonObject } from '../manifest.js';
11
+ import { isLazy, isLazyWitness, lazyOf } from '../plugin-contract.js';
12
+ import { withPhaseSpan, withPluginHookSpan } from './otel.js';
12
13
  const debugKernel = createDebugLogger('arki:dot:kernel');
13
14
  const DOCS_BASE = 'https://docs.arki.dev/dot/diagnostics';
14
15
  /** Resolve a needs-shape witness to its wire key. */
@@ -22,7 +23,7 @@ function makeIssue(args) {
22
23
  return {
23
24
  code: args.code,
24
25
  severity: args.severity ?? 'error',
25
- pip: args.pip,
26
+ plugin: args.plugin,
26
27
  message: args.message,
27
28
  remediation: args.remediation,
28
29
  docsUrl: `${DOCS_BASE}#${args.docsAnchor}`,
@@ -37,7 +38,7 @@ export class DotAppImpl {
37
38
  #appName;
38
39
  #appVersion;
39
40
  #config;
40
- /** Pips in declaration order — v2 boot order IS declaration order. */
41
+ /** Plugins in declaration order — v2 boot order IS declaration order. */
41
42
  #ordered;
42
43
  #records;
43
44
  /** Macro-state of the app. */
@@ -46,9 +47,9 @@ export class DotAppImpl {
46
47
  #hookTimeoutMs;
47
48
  /** Manifest finalised after `configure`. */
48
49
  #manifest;
49
- /** Wire-keyed services map — populated as pips boot. */
50
+ /** Wire-keyed services map — populated as plugins boot. */
50
51
  #serviceMap = new Map();
51
- /** Which pip provided each wire key — feeds dependency edges + collisions. */
52
+ /** Which plugin provided each wire key — feeds dependency edges + collisions. */
52
53
  #providerByWireKey = new Map();
53
54
  /** Dependency edges observed during boot (consumer → provider). */
54
55
  #wiringEdges = [];
@@ -77,7 +78,7 @@ export class DotAppImpl {
77
78
  * Structured lifecycle logger. One per app instance; named so consumers
78
79
  * can elevate it to DEBUG via `DEBUG=arki:dot:lifecycle` without
79
80
  * touching unrelated namespaces. Every line carries the app name plus
80
- * any phase/pip attributes from the call site. The span helpers
81
+ * any phase/plugin attributes from the call site. The span helpers
81
82
  * (see `./otel.ts`) thread the active span's `traceId`+`spanId` onto
82
83
  * forked instances of this logger, so every log record is groupable
83
84
  * with its trace in any OTel-compatible backend.
@@ -99,47 +100,48 @@ export class DotAppImpl {
99
100
  this.#observers = new Set(config.observers);
100
101
  // Declaration order is boot order in v2. Duplicate names are surfaced at
101
102
  // construction (the `configure` phase pseudo-time).
102
- debugKernel('[%s] registering %d pip(s) in declaration order', this.#appName, config.pips.length);
103
- this.#ordered = [...config.pips];
103
+ debugKernel('[%s] registering %d plugin(s) in declaration order', this.#appName, config.plugins.length);
104
+ this.#ordered = [...config.plugins];
104
105
  this.#records = new Map();
105
- for (const [order, pip] of this.#ordered.entries()) {
106
- if (this.#records.has(pip.name)) {
106
+ for (const [order, plugin] of this.#ordered.entries()) {
107
+ if (this.#records.has(plugin.name)) {
107
108
  throw new DotLifecycleError({
108
- code: DotLifecycleErrorCode.DuplicatePip,
109
+ code: DotLifecycleErrorCode.DuplicatePlugin,
109
110
  phase: 'configure',
110
- pip: pip.name,
111
- message: `Pip "${pip.name}" is registered twice`,
111
+ plugin: plugin.name,
112
+ message: `Plugin "${plugin.name}" is registered twice`,
112
113
  });
113
114
  }
114
- // `$` is the kernel context namespace ($app/$pip/$config). The pip()
115
+ // `$` is the kernel context namespace ($app/$plugin/$config). The plugin()
115
116
  // constraint bans these at compile time; this is the runtime backstop
116
- // for erased pips and rename() targets, which types cannot see.
117
- for (const alias of Object.keys(pip.needs)) {
117
+ // for erased plugins and rename() targets, which types cannot see.
118
+ for (const alias of Object.keys(plugin.needs)) {
118
119
  if (alias.startsWith('$')) {
119
120
  throw new DotLifecycleError({
120
121
  code: DotLifecycleErrorCode.ReservedServiceKey,
121
122
  phase: 'configure',
122
- pip: pip.name,
123
- message: `Pip "${pip.name}" declares needs alias "${alias}" — the "$" prefix is reserved ` +
124
- `for kernel context keys ($app, $pip, $config). Rename the alias.`,
123
+ plugin: plugin.name,
124
+ message: `Plugin "${plugin.name}" declares needs alias "${alias}" — the "$" prefix is reserved ` +
125
+ `for kernel context keys ($app, $plugin, $config). Rename the alias.`,
125
126
  });
126
127
  }
127
128
  }
128
- for (const [localKey, wireKey] of Object.entries(pip.renames)) {
129
+ for (const [localKey, wireKey] of Object.entries(plugin.renames)) {
129
130
  if (wireKey.startsWith('$')) {
130
131
  throw new DotLifecycleError({
131
132
  code: DotLifecycleErrorCode.ReservedServiceKey,
132
133
  phase: 'configure',
133
- pip: pip.name,
134
- message: `Pip "${pip.name}" renames "${localKey}" to "${wireKey}" — the "$" prefix is ` +
135
- `reserved for kernel context keys ($app, $pip, $config). Pick a different wire key.`,
134
+ plugin: plugin.name,
135
+ message: `Plugin "${plugin.name}" renames "${localKey}" to "${wireKey}" — the "$" prefix is ` +
136
+ `reserved for kernel context keys ($app, $plugin, $config). Pick a different wire key.`,
136
137
  });
137
138
  }
138
139
  }
139
- this.#records.set(pip.name, {
140
- pip,
140
+ this.#records.set(plugin.name, {
141
+ plugin,
141
142
  order,
142
- routes: [],
143
+ actions: [],
144
+ projections: [],
143
145
  services: [],
144
146
  hooks: new Set(),
145
147
  provides: new Set(),
@@ -199,13 +201,13 @@ export class DotAppImpl {
199
201
  * Emit a single hook-level event. Skipped (zero allocation) when no
200
202
  * observers are registered.
201
203
  */
202
- #emitHook(phase, pip, order, status, opts) {
204
+ #emitHook(phase, plugin, order, status, opts) {
203
205
  if (this.#observers.size === 0)
204
206
  return;
205
207
  const event = {
206
- kind: 'pip-hook',
208
+ kind: 'plugin-hook',
207
209
  phase,
208
- pip,
210
+ plugin,
209
211
  order,
210
212
  status,
211
213
  appName: this.#appName,
@@ -215,6 +217,89 @@ export class DotAppImpl {
215
217
  };
216
218
  this.#emit(event);
217
219
  }
220
+ #registerAction(record, action) {
221
+ let meta;
222
+ if (action.meta !== undefined) {
223
+ try {
224
+ meta = toJsonObject(action.meta);
225
+ }
226
+ catch (error) {
227
+ const issue = makeIssue({
228
+ code: DotLifecycleErrorCode.ActionMetaNotJson,
229
+ plugin: record.plugin.name,
230
+ message: `action "${action.id}" for binding "${action.binding}" has metadata that is not JSON-round-trip safe: ` +
231
+ stringifyError(error),
232
+ remediation: 'Only put JSON-serializable data in action meta. Convert Dates to strings, remove functions/undefined fields, and avoid NaN/Infinity.',
233
+ docsAnchor: 'action-meta-not-json',
234
+ metadata: { actionId: action.id, binding: action.binding },
235
+ });
236
+ record.issues.push(issue);
237
+ }
238
+ }
239
+ const stamped = {
240
+ id: action.id,
241
+ plugin: record.plugin.name,
242
+ binding: action.binding,
243
+ direction: action.direction,
244
+ ...(action.address === undefined ? {} : { address: action.address }),
245
+ ...(action.summary === undefined ? {} : { summary: action.summary }),
246
+ ...(meta === undefined ? {} : { meta }),
247
+ ...(action.metaSchema === undefined ? {} : { metaSchema: action.metaSchema }),
248
+ };
249
+ for (const existingRecord of this.#records.values()) {
250
+ const duplicate = existingRecord.actions.find(existing => existing.binding === stamped.binding && existing.id === stamped.id);
251
+ if (duplicate === undefined)
252
+ continue;
253
+ const issue = makeIssue({
254
+ code: DotLifecycleErrorCode.DuplicateAction,
255
+ plugin: record.plugin.name,
256
+ message: `action "${stamped.id}" is registered more than once for binding "${stamped.binding}".`,
257
+ remediation: 'Action ids must be unique within a binding. Rename one action or move one of them to a different binding.',
258
+ docsAnchor: 'duplicate-action',
259
+ metadata: { binding: stamped.binding, id: stamped.id, firstPlugin: duplicate.plugin },
260
+ });
261
+ record.issues.push(issue);
262
+ break;
263
+ }
264
+ record.actions.push(stamped);
265
+ }
266
+ #registerProjection(record, projection) {
267
+ record.projections.push({ ...projection, plugin: record.plugin.name });
268
+ }
269
+ #registerRouteShim(record, route) {
270
+ const meta = {};
271
+ if (route.method !== undefined)
272
+ meta['method'] = route.method;
273
+ if (route.path !== undefined)
274
+ meta['path'] = route.path;
275
+ if (route.input !== undefined) {
276
+ const input = {};
277
+ if (route.input.query !== undefined)
278
+ input['query'] = route.input.query;
279
+ if (route.input.body !== undefined)
280
+ input['body'] = route.input.body;
281
+ if (Object.keys(input).length > 0)
282
+ meta['input'] = input;
283
+ }
284
+ if (route.output !== undefined)
285
+ meta['output'] = route.output;
286
+ if (route.streaming !== undefined)
287
+ meta['streaming'] = route.streaming;
288
+ this.#registerAction(record, {
289
+ id: route.id,
290
+ binding: route.transport,
291
+ direction: 'in',
292
+ ...(route.path === undefined ? {} : { address: `${route.method ?? 'GET'} ${route.path}` }),
293
+ ...(route.description === undefined ? {} : { summary: route.description }),
294
+ ...(Object.keys(meta).length === 0 ? {} : { meta }),
295
+ });
296
+ }
297
+ #actionFromSource(source) {
298
+ if (hasToDotAction(source)) {
299
+ return source.toDotAction();
300
+ }
301
+ return source;
302
+ }
218
303
  /**
219
304
  * Wrap a sync phase body with starting/completed/failed observer events.
220
305
  * `configure` is the only sync phase in the kernel today — keep the
@@ -314,12 +399,12 @@ export class DotAppImpl {
314
399
  throw this.#reuseError('configure');
315
400
  }
316
401
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'configure');
317
- phaseLogger.debug('configure: starting', { 'dot.app.pip.count': this.#ordered.length });
402
+ phaseLogger.debug('configure: starting', { 'dot.app.plugin.count': this.#ordered.length });
318
403
  this.#withPhaseEmit('configure', () => {
319
404
  withPhaseSpan({
320
405
  appName: this.#appName,
321
406
  phase: 'configure',
322
- pipCount: this.#ordered.length,
407
+ pluginCount: this.#ordered.length,
323
408
  logger: phaseLogger,
324
409
  }, () => this.#runConfigureInner(phaseLogger));
325
410
  });
@@ -330,23 +415,57 @@ export class DotAppImpl {
330
415
  * intricate error-handling readable.
331
416
  */
332
417
  #runConfigureInner(phaseLogger) {
333
- for (const pip of this.#ordered) {
334
- const record = this.#records.get(pip.name);
335
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', pip.name);
336
- if (!pip.hooks.configure) {
337
- pipLogger.debug('configure: skipped (no hook)');
418
+ for (const plugin of this.#ordered) {
419
+ const record = this.#records.get(plugin.name);
420
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
421
+ const declaredStarted = performance.now();
422
+ try {
423
+ for (const source of plugin.actions)
424
+ this.#registerAction(record, this.#actionFromSource(source));
425
+ }
426
+ catch (error) {
427
+ const durationMs = performance.now() - declaredStarted;
428
+ const issue = makeIssue({
429
+ code: DotLifecycleErrorCode.ConfigureFailed,
430
+ plugin: plugin.name,
431
+ message: `action declaration threw for plugin "${plugin.name}": ${stringifyError(error)}`,
432
+ remediation: `Fix the action declarations of "${plugin.name}". Declarative actions must be static and synchronous.`,
433
+ docsAnchor: 'configure-failed',
434
+ });
435
+ record.issues.push(issue);
436
+ record.lifecycleDiagnostics.push({
437
+ plugin: plugin.name,
438
+ hook: 'configure',
439
+ state: 'failed',
440
+ order: record.order,
441
+ durationMs,
442
+ issues: [issue],
443
+ });
444
+ this.#emitHook('configure', plugin.name, record.order, 'failed', { durationMs, error });
445
+ this.#state = 'failed';
446
+ this.#manifest = this.#buildManifest();
447
+ throw new DotLifecycleError({
448
+ code: DotLifecycleErrorCode.ConfigureFailed,
449
+ phase: 'configure',
450
+ plugin: plugin.name,
451
+ message: issue.message,
452
+ cause: error,
453
+ });
454
+ }
455
+ if (!plugin.hooks.configure) {
456
+ pluginLogger.debug('configure: skipped (no hook)');
338
457
  continue;
339
458
  }
340
459
  record.hooks.add('configure');
341
460
  const ctx = {
342
- pipName: pip.name,
461
+ pluginName: plugin.name,
343
462
  appName: this.#appName,
344
463
  registerService: (name, kind) => {
345
- record.services.push({ name, pip: pip.name, kind });
346
- },
347
- registerRoute: route => {
348
- record.routes.push({ ...route, pip: pip.name });
464
+ record.services.push({ name, plugin: plugin.name, kind });
349
465
  },
466
+ registerAction: action => this.#registerAction(record, action),
467
+ registerProjection: projection => this.#registerProjection(record, projection),
468
+ registerRoute: route => this.#registerRouteShim(record, route),
350
469
  registerLifecycleHook: hook => {
351
470
  record.hooks.add(hook);
352
471
  },
@@ -356,43 +475,43 @@ export class DotAppImpl {
356
475
  },
357
476
  };
358
477
  const started = performance.now();
359
- this.#emitHook('configure', pip.name, record.order, 'starting');
478
+ this.#emitHook('configure', plugin.name, record.order, 'starting');
360
479
  let returned;
361
480
  try {
362
- returned = withPipHookSpan({
481
+ returned = withPluginHookSpan({
363
482
  appName: this.#appName,
364
- pipName: pip.name,
365
- pipVersion: pip.version,
483
+ pluginName: plugin.name,
484
+ pluginVersion: plugin.version,
366
485
  hook: 'configure',
367
486
  order: record.order,
368
- logger: pipLogger,
369
- }, () => pip.hooks.configure(ctx));
487
+ logger: pluginLogger,
488
+ }, () => plugin.hooks.configure(ctx));
370
489
  }
371
490
  catch (error) {
372
491
  const durationMs = performance.now() - started;
373
492
  const issue = makeIssue({
374
493
  code: DotLifecycleErrorCode.ConfigureFailed,
375
- pip: pip.name,
376
- message: `configure hook threw for pip "${pip.name}": ${stringifyError(error)}`,
377
- remediation: `Fix the error in the configure() hook of "${pip.name}". configure() is for synchronous registration only — avoid throwing on declarative work.`,
494
+ plugin: plugin.name,
495
+ message: `configure hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
496
+ remediation: `Fix the error in the configure() hook of "${plugin.name}". configure() is for synchronous registration only — avoid throwing on declarative work.`,
378
497
  docsAnchor: 'configure-failed',
379
498
  });
380
499
  record.issues.push(issue);
381
500
  record.lifecycleDiagnostics.push({
382
- pip: pip.name,
501
+ plugin: plugin.name,
383
502
  hook: 'configure',
384
503
  state: 'failed',
385
504
  order: record.order,
386
505
  durationMs,
387
506
  issues: [issue],
388
507
  });
389
- this.#emitHook('configure', pip.name, record.order, 'failed', { durationMs, error });
508
+ this.#emitHook('configure', plugin.name, record.order, 'failed', { durationMs, error });
390
509
  this.#state = 'failed';
391
510
  this.#manifest = this.#buildManifest();
392
511
  throw new DotLifecycleError({
393
512
  code: DotLifecycleErrorCode.ConfigureFailed,
394
513
  phase: 'configure',
395
- pip: pip.name,
514
+ plugin: plugin.name,
396
515
  message: issue.message,
397
516
  cause: error,
398
517
  });
@@ -401,21 +520,21 @@ export class DotAppImpl {
401
520
  const durationMs = performance.now() - started;
402
521
  const issue = makeIssue({
403
522
  code: DotLifecycleErrorCode.ConfigureAsync,
404
- pip: pip.name,
405
- message: `configure hook of pip "${pip.name}" returned a Promise. configure must be synchronous.`,
523
+ plugin: plugin.name,
524
+ message: `configure hook of plugin "${plugin.name}" returned a Promise. configure must be synchronous.`,
406
525
  remediation: 'Move async work to the boot() hook. configure() is for synchronous registration only.',
407
526
  docsAnchor: 'configure-async',
408
527
  });
409
528
  record.issues.push(issue);
410
529
  record.lifecycleDiagnostics.push({
411
- pip: pip.name,
530
+ plugin: plugin.name,
412
531
  hook: 'configure',
413
532
  state: 'failed',
414
533
  order: record.order,
415
534
  durationMs,
416
535
  issues: [issue],
417
536
  });
418
- this.#emitHook('configure', pip.name, record.order, 'failed', {
537
+ this.#emitHook('configure', plugin.name, record.order, 'failed', {
419
538
  durationMs,
420
539
  error: new Error(issue.message),
421
540
  });
@@ -424,39 +543,39 @@ export class DotAppImpl {
424
543
  throw new DotLifecycleError({
425
544
  code: DotLifecycleErrorCode.ConfigureAsync,
426
545
  phase: 'configure',
427
- pip: pip.name,
546
+ plugin: plugin.name,
428
547
  message: issue.message,
429
548
  });
430
549
  }
431
550
  const durationMs = performance.now() - started;
432
551
  record.lifecycleDiagnostics.push({
433
- pip: pip.name,
552
+ plugin: plugin.name,
434
553
  hook: 'configure',
435
554
  state: 'configured',
436
555
  order: record.order,
437
556
  durationMs,
438
557
  issues: [],
439
558
  });
440
- this.#emitHook('configure', pip.name, record.order, 'completed', { durationMs });
441
- pipLogger.debug('configure: done', { 'dot.pip.duration.ms': durationMs });
559
+ this.#emitHook('configure', plugin.name, record.order, 'completed', { durationMs });
560
+ pluginLogger.debug('configure: done', { 'dot.plugin.duration.ms': durationMs });
442
561
  }
443
562
  // Also declare lifecycle-hook participation for non-configure hooks present
444
- // on the pip object, so the manifest reflects them.
445
- for (const pip of this.#ordered) {
446
- const record = this.#records.get(pip.name);
447
- if (pip.hooks.boot)
563
+ // on the plugin object, so the manifest reflects them.
564
+ for (const plugin of this.#ordered) {
565
+ const record = this.#records.get(plugin.name);
566
+ if (plugin.hooks.boot)
448
567
  record.hooks.add('boot');
449
- if (pip.hooks.start)
568
+ if (plugin.hooks.start)
450
569
  record.hooks.add('start');
451
- if (pip.hooks.stop)
570
+ if (plugin.hooks.stop)
452
571
  record.hooks.add('stop');
453
- if (pip.hooks.dispose)
572
+ if (plugin.hooks.dispose)
454
573
  record.hooks.add('dispose');
455
574
  }
456
575
  this.#configured = true;
457
576
  this.#state = 'configured';
458
577
  this.#manifest = this.#buildManifest();
459
- phaseLogger.debug('configure: complete', { 'dot.app.pip.count': this.#ordered.length });
578
+ phaseLogger.debug('configure: complete', { 'dot.app.plugin.count': this.#ordered.length });
460
579
  }
461
580
  /**
462
581
  * Run one lifecycle transition after all previously enqueued ones.
@@ -495,42 +614,42 @@ export class DotAppImpl {
495
614
  this.runConfigure();
496
615
  }
497
616
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'boot');
498
- phaseLogger.debug('boot: starting', { 'dot.app.pip.count': this.#ordered.length });
617
+ phaseLogger.debug('boot: starting', { 'dot.app.plugin.count': this.#ordered.length });
499
618
  return this.#withPhaseEmitAsync('boot', () => withPhaseSpan({
500
619
  appName: this.#appName,
501
620
  phase: 'boot',
502
- pipCount: this.#ordered.length,
621
+ pluginCount: this.#ordered.length,
503
622
  logger: phaseLogger,
504
623
  }, () => this.#runBootInner(phaseLogger)));
505
624
  }
506
625
  /**
507
- * Resolve a pip's needs into a hook context (alias-keyed), joined with
508
- * the `$`-prefixed kernel keys and — for post-boot hooks — the pip's own
626
+ * Resolve a plugin's needs into a hook context (alias-keyed), joined with
627
+ * the `$`-prefixed kernel keys and — for post-boot hooks — the plugin's own
509
628
  * published services (local keys).
510
629
  *
511
630
  * @throws {DotLifecycleError} `DOT_LIFECYCLE_E012` when a need has no
512
- * provider among earlier-booted pips. Teardown hooks can never hit
631
+ * provider among earlier-booted plugins. Teardown hooks can never hit
513
632
  * this: the service map only grows, and reverse-order teardown keeps
514
633
  * providers alive until their consumers are done.
515
634
  */
516
635
  #buildHookCtx(record, phase, includeOwnProvides) {
517
636
  const ctx = {
518
637
  $app: this.#appName,
519
- $pip: record.pip.name,
638
+ $plugin: record.plugin.name,
520
639
  $config: this.#config,
521
640
  };
522
- for (const [alias, witness] of Object.entries(record.pip.needs)) {
641
+ for (const [alias, witness] of Object.entries(record.plugin.needs)) {
523
642
  const wireKey = wireKeyOf(witness, alias);
524
643
  if (!this.#serviceMap.has(wireKey)) {
525
644
  throw new DotLifecycleError({
526
645
  code: DotLifecycleErrorCode.UnsatisfiedNeed,
527
646
  phase,
528
- pip: record.pip.name,
529
- message: `Pip "${record.pip.name}" needs service "${wireKey}" but no earlier pip provides it. ` +
530
- `Register a provider with .use() before this pip. Services flow strictly forward — ` +
531
- `if a later pip provides "${wireKey}", move that provider earlier; if two pips need ` +
647
+ plugin: record.plugin.name,
648
+ message: `Plugin "${record.plugin.name}" needs service "${wireKey}" but no earlier plugin provides it. ` +
649
+ `Register a provider with .use() before this plugin. Services flow strictly forward — ` +
650
+ `if a later plugin provides "${wireKey}", move that provider earlier; if two plugins need ` +
532
651
  `each other's services, that is a cycle: merge them or extract the shared piece into ` +
533
- `a third pip both consume.`,
652
+ `a third plugin both consume.`,
534
653
  });
535
654
  }
536
655
  const value = this.#serviceMap.get(wireKey);
@@ -541,8 +660,8 @@ export class DotAppImpl {
541
660
  // the underlying value's lifecycle stays with its provider.
542
661
  ctx[alias] = isLazyWitness(witness) && !isLazy(value) ? lazyOf(value) : value;
543
662
  const provider = this.#providerByWireKey.get(wireKey);
544
- if (provider !== undefined && !this.#wiringEdges.some(e => e.from === record.pip.name && e.to === provider)) {
545
- this.#wiringEdges.push({ from: record.pip.name, to: provider, kind: 'requires' });
663
+ if (provider !== undefined && !this.#wiringEdges.some(e => e.from === record.plugin.name && e.to === provider)) {
664
+ this.#wiringEdges.push({ from: record.plugin.name, to: provider, kind: 'requires' });
546
665
  }
547
666
  }
548
667
  if (includeOwnProvides) {
@@ -553,13 +672,13 @@ export class DotAppImpl {
553
672
  /**
554
673
  * Race one hook invocation against the app's `hookTimeoutMs` watchdog.
555
674
  * No budget configured → plain pass-through. On timeout the returned
556
- * promise rejects with `DOT_LIFECYCLE_E015` naming the pip and hook,
675
+ * promise rejects with `DOT_LIFECYCLE_E015` naming the plugin and hook,
557
676
  * and the call site's existing catch path applies the phase's normal
558
677
  * failure rules (boot rollback, start cascade, teardown aggregation).
559
678
  * The hook's own promise cannot be cancelled — the watchdog makes a
560
679
  * hang visible and bounded; it does not kill the hung work.
561
680
  */
562
- async #withHookBudget(pipName, hook, run) {
681
+ async #withHookBudget(pluginName, hook, run) {
563
682
  const budget = this.#hookTimeoutMs;
564
683
  if (budget === undefined)
565
684
  return run();
@@ -569,8 +688,8 @@ export class DotAppImpl {
569
688
  reject(new DotLifecycleError({
570
689
  code: DotLifecycleErrorCode.HookTimeout,
571
690
  phase: hook,
572
- pip: pipName,
573
- message: `${hook} hook of pip "${pipName}" exceeded the ${budget.toString()}ms hookTimeoutMs watchdog. ` +
691
+ plugin: pluginName,
692
+ message: `${hook} hook of plugin "${pluginName}" exceeded the ${budget.toString()}ms hookTimeoutMs watchdog. ` +
574
693
  `The kernel treats the hook as failed and applies its normal rollback/aggregation rules, but ` +
575
694
  `cannot cancel the hook's promise — find the hang (a missing await? a connection that never ` +
576
695
  `settles?) or raise the budget in defineApp(name, { hookTimeoutMs }).`,
@@ -598,26 +717,26 @@ export class DotAppImpl {
598
717
  const fail = async (args) => {
599
718
  const issue = makeIssue({
600
719
  code: args.code,
601
- pip: args.record.pip.name,
720
+ plugin: args.record.plugin.name,
602
721
  message: args.message,
603
722
  remediation: args.remediation,
604
723
  docsAnchor: args.docsAnchor,
605
724
  });
606
725
  args.record.issues.push(issue);
607
726
  args.record.lifecycleDiagnostics.push({
608
- pip: args.record.pip.name,
727
+ plugin: args.record.plugin.name,
609
728
  hook: 'boot',
610
729
  state: 'failed',
611
730
  order: args.record.order,
612
731
  durationMs: args.durationMs,
613
732
  issues: [issue],
614
733
  });
615
- this.#emitHook('boot', args.record.pip.name, args.record.order, 'failed', {
734
+ this.#emitHook('boot', args.record.plugin.name, args.record.order, 'failed', {
616
735
  durationMs: args.durationMs,
617
736
  error: args.cause ?? new Error(args.message),
618
737
  });
619
- phaseLogger.error('boot: FAILED — rolling back already-booted pips', {
620
- 'dot.pip.name': args.record.pip.name,
738
+ phaseLogger.error('boot: FAILED — rolling back already-booted plugins', {
739
+ 'dot.plugin.name': args.record.plugin.name,
621
740
  'dot.app.rollback.count': args.rollback.length,
622
741
  });
623
742
  const disposeFailures = await this.#runDisposeForRecords(reverseRecords(args.rollback), phaseLogger);
@@ -626,17 +745,17 @@ export class DotAppImpl {
626
745
  throw new DotLifecycleError({
627
746
  code: args.code,
628
747
  phase: 'boot',
629
- pip: args.record.pip.name,
748
+ plugin: args.record.plugin.name,
630
749
  message: args.message,
631
750
  cause: args.cause,
632
751
  failures: disposeFailures.length > 0 ? disposeFailures : undefined,
633
752
  });
634
753
  };
635
- for (const pip of this.#ordered) {
636
- const record = this.#records.get(pip.name);
637
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', pip.name);
754
+ for (const plugin of this.#ordered) {
755
+ const record = this.#records.get(plugin.name);
756
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
638
757
  const started = performance.now();
639
- // Resolve needs BEFORE invoking the hook — a pip with needs but no
758
+ // Resolve needs BEFORE invoking the hook — a plugin with needs but no
640
759
  // boot hook still fails fast when its wiring is unsatisfied.
641
760
  let ctx;
642
761
  try {
@@ -648,36 +767,36 @@ export class DotAppImpl {
648
767
  record,
649
768
  code: DotLifecycleErrorCode.UnsatisfiedNeed,
650
769
  message,
651
- remediation: `Add a provider for the missing service with .use() before "${pip.name}", or rename an ` +
652
- `existing provider's keys to match. If a later pip provides it, reorder — services flow ` +
653
- `strictly forward. Mutual needs between two pips are a cycle: merge them or extract the ` +
654
- `shared piece into a third pip both consume.`,
770
+ remediation: `Add a provider for the missing service with .use() before "${plugin.name}", or rename an ` +
771
+ `existing provider's keys to match. If a later plugin provides it, reorder — services flow ` +
772
+ `strictly forward. Mutual needs between two plugins are a cycle: merge them or extract the ` +
773
+ `shared piece into a third plugin both consume.`,
655
774
  docsAnchor: 'unsatisfied-need',
656
775
  durationMs: performance.now() - started,
657
776
  rollback: bootedRecords,
658
777
  });
659
778
  }
660
- if (!pip.hooks.boot) {
779
+ if (!plugin.hooks.boot) {
661
780
  record.booted = true;
662
781
  bootedRecords.push(record);
663
- pipLogger.debug('boot: skipped (no hook)');
782
+ pluginLogger.debug('boot: skipped (no hook)');
664
783
  continue;
665
784
  }
666
785
  record.hooks.add('boot');
667
- this.#emitHook('boot', pip.name, record.order, 'starting');
786
+ this.#emitHook('boot', plugin.name, record.order, 'starting');
668
787
  let result;
669
788
  try {
670
- result = await this.#withHookBudget(pip.name, 'boot', () => withPipHookSpan({
789
+ result = await this.#withHookBudget(plugin.name, 'boot', () => withPluginHookSpan({
671
790
  appName: this.#appName,
672
- pipName: pip.name,
673
- pipVersion: pip.version,
791
+ pluginName: plugin.name,
792
+ pluginVersion: plugin.version,
674
793
  hook: 'boot',
675
794
  order: record.order,
676
- logger: pipLogger,
795
+ logger: pluginLogger,
677
796
  },
678
797
  // Erasure boundary: hooks are stored as `(ctx: never) => ...`;
679
798
  // the kernel is the one caller allowed to cross it.
680
- () => pip.hooks.boot(ctx)));
799
+ () => plugin.hooks.boot(ctx)));
681
800
  }
682
801
  catch (error) {
683
802
  const timedOut = error instanceof DotLifecycleError && error.code === DotLifecycleErrorCode.HookTimeout;
@@ -686,10 +805,10 @@ export class DotAppImpl {
686
805
  code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.BootFailed,
687
806
  message: timedOut
688
807
  ? error.message
689
- : `boot hook threw for pip "${pip.name}": ${stringifyError(error)}`,
808
+ : `boot hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
690
809
  remediation: timedOut
691
- ? `Find the hang in the boot() hook of "${pip.name}" — a missing await or a connection that never settles — or raise defineApp's hookTimeoutMs.`
692
- : `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.`,
810
+ ? `Find the hang in the boot() hook of "${plugin.name}" — a missing await or a connection that never settles — or raise defineApp's hookTimeoutMs.`
811
+ : `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.`,
693
812
  docsAnchor: timedOut ? 'hook-timeout' : 'boot-failed',
694
813
  durationMs: performance.now() - started,
695
814
  cause: error,
@@ -700,17 +819,17 @@ export class DotAppImpl {
700
819
  record.publishedServices = publishedServices;
701
820
  record.booted = true;
702
821
  for (const [localKey, value] of Object.entries(publishedServices)) {
703
- const wireKey = pip.renames[localKey] ?? localKey;
822
+ const wireKey = plugin.renames[localKey] ?? localKey;
704
823
  if (localKey.startsWith('$') || wireKey.startsWith('$')) {
705
- // The pip() constraint bans this at compile time; erased pips can
706
- // still reach here. A `$` publish would shadow $app/$pip/$config
707
- // in the pip's own post-boot hook contexts.
824
+ // The plugin() constraint bans this at compile time; erased plugins can
825
+ // still reach here. A `$` publish would shadow $app/$plugin/$config
826
+ // in the plugin's own post-boot hook contexts.
708
827
  return fail({
709
828
  record,
710
829
  code: DotLifecycleErrorCode.ReservedServiceKey,
711
- message: `Pip "${pip.name}" publishes service "${wireKey}" — the "$" prefix is reserved ` +
712
- `for kernel context keys ($app, $pip, $config).`,
713
- remediation: `Rename the "${wireKey}" key returned from the boot() hook of "${pip.name}" — "$"-prefixed keys would shadow the kernel context.`,
830
+ message: `Plugin "${plugin.name}" publishes service "${wireKey}" — the "$" prefix is reserved ` +
831
+ `for kernel context keys ($app, $plugin, $config).`,
832
+ remediation: `Rename the "${wireKey}" key returned from the boot() hook of "${plugin.name}" — "$"-prefixed keys would shadow the kernel context.`,
714
833
  docsAnchor: 'reserved-service-key',
715
834
  durationMs: performance.now() - started,
716
835
  rollback: [...bootedRecords, record],
@@ -718,41 +837,41 @@ export class DotAppImpl {
718
837
  }
719
838
  if (this.#serviceMap.has(wireKey)) {
720
839
  const owner = this.#providerByWireKey.get(wireKey) ?? 'unknown';
721
- // The current pip HAS booted — include it in the rollback.
840
+ // The current plugin HAS booted — include it in the rollback.
722
841
  return fail({
723
842
  record,
724
843
  code: DotLifecycleErrorCode.ServiceCollision,
725
- message: `Pip "${pip.name}" publishes service "${wireKey}" which pip "${owner}" already provides.`,
726
- remediation: `Mount one of the two with rename(pip, { '${wireKey}': '<newKey>' }) to keep both instances, or remove the duplicate provider.`,
844
+ message: `Plugin "${plugin.name}" publishes service "${wireKey}" which plugin "${owner}" already provides.`,
845
+ remediation: `Mount one of the two with rename(plugin, { '${wireKey}': '<newKey>' }) to keep both instances, or remove the duplicate provider.`,
727
846
  docsAnchor: 'service-collision',
728
847
  durationMs: performance.now() - started,
729
848
  rollback: [...bootedRecords, record],
730
849
  });
731
850
  }
732
851
  this.#serviceMap.set(wireKey, value);
733
- this.#providerByWireKey.set(wireKey, pip.name);
852
+ this.#providerByWireKey.set(wireKey, plugin.name);
734
853
  this.#aggregatedServices[wireKey] = value;
735
854
  record.provides.add(wireKey);
736
855
  }
737
856
  bootedRecords.push(record);
738
857
  const durationMs = performance.now() - started;
739
858
  record.lifecycleDiagnostics.push({
740
- pip: pip.name,
859
+ plugin: plugin.name,
741
860
  hook: 'boot',
742
861
  state: 'booted',
743
862
  order: record.order,
744
863
  durationMs,
745
864
  issues: [],
746
865
  });
747
- this.#emitHook('boot', pip.name, record.order, 'completed', { durationMs });
748
- pipLogger.debug('boot: done', {
749
- 'dot.pip.duration.ms': durationMs,
750
- 'dot.pip.services.published': Object.keys(publishedServices).length,
866
+ this.#emitHook('boot', plugin.name, record.order, 'completed', { durationMs });
867
+ pluginLogger.debug('boot: done', {
868
+ 'dot.plugin.duration.ms': durationMs,
869
+ 'dot.plugin.services.published': Object.keys(publishedServices).length,
751
870
  });
752
871
  }
753
872
  this.#state = 'booted';
754
873
  this.#manifest = this.#buildManifest();
755
- phaseLogger.debug('boot: complete', { 'dot.app.pip.count': this.#ordered.length });
874
+ phaseLogger.debug('boot: complete', { 'dot.app.plugin.count': this.#ordered.length });
756
875
  }
757
876
  /** Public start(). Boots first if needed. Idempotent + concurrent-safe. */
758
877
  async start() {
@@ -778,11 +897,11 @@ export class DotAppImpl {
778
897
  }
779
898
  // From booted or stopped, run start hooks.
780
899
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'start');
781
- phaseLogger.debug('start: starting', { 'dot.app.pip.count': this.#ordered.length });
900
+ phaseLogger.debug('start: starting', { 'dot.app.plugin.count': this.#ordered.length });
782
901
  return this.#withPhaseEmitAsync('start', () => withPhaseSpan({
783
902
  appName: this.#appName,
784
903
  phase: 'start',
785
- pipCount: this.#ordered.length,
904
+ pluginCount: this.#ordered.length,
786
905
  logger: phaseLogger,
787
906
  }, () => this.#runStartInner(phaseLogger)));
788
907
  }
@@ -792,52 +911,52 @@ export class DotAppImpl {
792
911
  */
793
912
  async #runStartInner(phaseLogger) {
794
913
  const startedRecords = [];
795
- for (const pip of this.#ordered) {
796
- const record = this.#records.get(pip.name);
797
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', pip.name);
798
- if (!pip.hooks.start) {
799
- pipLogger.debug('start: skipped (no hook)');
914
+ for (const plugin of this.#ordered) {
915
+ const record = this.#records.get(plugin.name);
916
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
917
+ if (!plugin.hooks.start) {
918
+ pluginLogger.debug('start: skipped (no hook)');
800
919
  continue;
801
920
  }
802
921
  record.hooks.add('start');
803
922
  const ctx = this.#buildHookCtx(record, 'start', true);
804
923
  const startedAt = performance.now();
805
- this.#emitHook('start', pip.name, record.order, 'starting');
924
+ this.#emitHook('start', plugin.name, record.order, 'starting');
806
925
  try {
807
- await this.#withHookBudget(pip.name, 'start', () => withPipHookSpan({
926
+ await this.#withHookBudget(plugin.name, 'start', () => withPluginHookSpan({
808
927
  appName: this.#appName,
809
- pipName: pip.name,
810
- pipVersion: pip.version,
928
+ pluginName: plugin.name,
929
+ pluginVersion: plugin.version,
811
930
  hook: 'start',
812
931
  order: record.order,
813
- logger: pipLogger,
814
- }, () => pip.hooks.start(ctx)));
932
+ logger: pluginLogger,
933
+ }, () => plugin.hooks.start(ctx)));
815
934
  }
816
935
  catch (error) {
817
936
  const durationMs = performance.now() - startedAt;
818
937
  const timedOut = error instanceof DotLifecycleError && error.code === DotLifecycleErrorCode.HookTimeout;
819
938
  const issue = makeIssue({
820
939
  code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.StartFailed,
821
- pip: pip.name,
940
+ plugin: plugin.name,
822
941
  message: timedOut
823
942
  ? error.message
824
- : `start hook threw for pip "${pip.name}": ${stringifyError(error)}`,
943
+ : `start hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
825
944
  remediation: timedOut
826
- ? `Find the hang in the start() hook of "${pip.name}" — or raise defineApp's hookTimeoutMs. DOT rolls back as for any start failure.`
827
- : `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.`,
945
+ ? `Find the hang in the start() hook of "${plugin.name}" — or raise defineApp's hookTimeoutMs. DOT rolls back as for any start failure.`
946
+ : `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.`,
828
947
  docsAnchor: timedOut ? 'hook-timeout' : 'start-failed',
829
948
  });
830
949
  record.issues.push(issue);
831
950
  record.lifecycleDiagnostics.push({
832
- pip: pip.name,
951
+ plugin: plugin.name,
833
952
  hook: 'start',
834
953
  state: 'failed',
835
954
  order: record.order,
836
955
  durationMs,
837
956
  issues: [issue],
838
957
  });
839
- this.#emitHook('start', pip.name, record.order, 'failed', { durationMs, error });
840
- pipLogger.error('start: FAILED — rolling back', {
958
+ this.#emitHook('start', plugin.name, record.order, 'failed', { durationMs, error });
959
+ pluginLogger.error('start: FAILED — rolling back', {
841
960
  'dot.app.rollback.started.count': startedRecords.length,
842
961
  });
843
962
  const stopFailures = await this.#runStopForRecords(reverseRecords(startedRecords), phaseLogger);
@@ -849,7 +968,7 @@ export class DotAppImpl {
849
968
  throw new DotLifecycleError({
850
969
  code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.StartFailed,
851
970
  phase: 'start',
852
- pip: pip.name,
971
+ plugin: plugin.name,
853
972
  message: issue.message,
854
973
  cause: error,
855
974
  failures: failures.length > 0 ? failures : undefined,
@@ -859,19 +978,19 @@ export class DotAppImpl {
859
978
  startedRecords.push(record);
860
979
  const durationMs = performance.now() - startedAt;
861
980
  record.lifecycleDiagnostics.push({
862
- pip: pip.name,
981
+ plugin: plugin.name,
863
982
  hook: 'start',
864
983
  state: 'started',
865
984
  order: record.order,
866
985
  durationMs,
867
986
  issues: [],
868
987
  });
869
- this.#emitHook('start', pip.name, record.order, 'completed', { durationMs });
870
- pipLogger.debug('start: done', { 'dot.pip.duration.ms': durationMs });
988
+ this.#emitHook('start', plugin.name, record.order, 'completed', { durationMs });
989
+ pluginLogger.debug('start: done', { 'dot.plugin.duration.ms': durationMs });
871
990
  }
872
991
  this.#state = 'started';
873
992
  this.#manifest = this.#buildManifest();
874
- phaseLogger.debug('start: complete', { 'dot.app.pip.count': this.#ordered.length });
993
+ phaseLogger.debug('start: complete', { 'dot.app.plugin.count': this.#ordered.length });
875
994
  }
876
995
  /** Public stop(). Idempotent + concurrent-safe. */
877
996
  async stop() {
@@ -901,11 +1020,11 @@ export class DotAppImpl {
901
1020
  }
902
1021
  async #runStop() {
903
1022
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'stop');
904
- phaseLogger.debug('stop: starting', { 'dot.app.pip.count': this.#ordered.length });
1023
+ phaseLogger.debug('stop: starting', { 'dot.app.plugin.count': this.#ordered.length });
905
1024
  return this.#withPhaseEmitAsync('stop', () => withPhaseSpan({
906
1025
  appName: this.#appName,
907
1026
  phase: 'stop',
908
- pipCount: this.#ordered.length,
1027
+ pluginCount: this.#ordered.length,
909
1028
  logger: phaseLogger,
910
1029
  }, async () => {
911
1030
  const startedRecords = this.#ordered.map(p => this.#records.get(p.name)).filter(r => r.started);
@@ -917,67 +1036,67 @@ export class DotAppImpl {
917
1036
  throw new DotLifecycleError({
918
1037
  code: DotLifecycleErrorCode.StopFailed,
919
1038
  phase: 'stop',
920
- message: `${failures.length} pip(s) failed during stop`,
1039
+ message: `${failures.length} plugin(s) failed during stop`,
921
1040
  failures,
922
1041
  });
923
1042
  }
924
- phaseLogger.debug('stop: complete', { 'dot.app.pip.count': this.#ordered.length });
1043
+ phaseLogger.debug('stop: complete', { 'dot.app.plugin.count': this.#ordered.length });
925
1044
  }));
926
1045
  }
927
1046
  async #runStopForRecords(records, phaseLogger) {
928
1047
  const failures = [];
929
1048
  for (const record of records) {
930
- if (!record.pip.hooks.stop)
1049
+ if (!record.plugin.hooks.stop)
931
1050
  continue;
932
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', record.pip.name);
1051
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', record.plugin.name);
933
1052
  record.hooks.add('stop');
934
1053
  const ctx = this.#buildHookCtx(record, 'stop', true);
935
1054
  const startedAt = performance.now();
936
- this.#emitHook('stop', record.pip.name, record.order, 'starting');
1055
+ this.#emitHook('stop', record.plugin.name, record.order, 'starting');
937
1056
  try {
938
- await this.#withHookBudget(record.pip.name, 'stop', () => withPipHookSpan({
1057
+ await this.#withHookBudget(record.plugin.name, 'stop', () => withPluginHookSpan({
939
1058
  appName: this.#appName,
940
- pipName: record.pip.name,
941
- pipVersion: record.pip.version,
1059
+ pluginName: record.plugin.name,
1060
+ pluginVersion: record.plugin.version,
942
1061
  hook: 'stop',
943
1062
  order: record.order,
944
- logger: pipLogger,
945
- }, () => record.pip.hooks.stop(ctx)));
1063
+ logger: pluginLogger,
1064
+ }, () => record.plugin.hooks.stop(ctx)));
946
1065
  record.started = false;
947
1066
  const durationMs = performance.now() - startedAt;
948
1067
  record.lifecycleDiagnostics.push({
949
- pip: record.pip.name,
1068
+ plugin: record.plugin.name,
950
1069
  hook: 'stop',
951
1070
  state: 'stopped',
952
1071
  order: record.order,
953
1072
  durationMs,
954
1073
  issues: [],
955
1074
  });
956
- this.#emitHook('stop', record.pip.name, record.order, 'completed', { durationMs });
957
- pipLogger.debug('stop: done', { 'dot.pip.duration.ms': durationMs });
1075
+ this.#emitHook('stop', record.plugin.name, record.order, 'completed', { durationMs });
1076
+ pluginLogger.debug('stop: done', { 'dot.plugin.duration.ms': durationMs });
958
1077
  }
959
1078
  catch (error) {
960
1079
  const durationMs = performance.now() - startedAt;
961
1080
  const issue = makeIssue({
962
1081
  code: DotLifecycleErrorCode.StopFailed,
963
- pip: record.pip.name,
964
- message: `stop hook threw for pip "${record.pip.name}": ${stringifyError(error)}`,
965
- remediation: `Fix the error in the stop() hook of "${record.pip.name}". Stop continues through individual failures and reports an aggregate error.`,
1082
+ plugin: record.plugin.name,
1083
+ message: `stop hook threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
1084
+ remediation: `Fix the error in the stop() hook of "${record.plugin.name}". Stop continues through individual failures and reports an aggregate error.`,
966
1085
  docsAnchor: 'stop-failed',
967
1086
  });
968
1087
  record.issues.push(issue);
969
1088
  record.lifecycleDiagnostics.push({
970
- pip: record.pip.name,
1089
+ plugin: record.plugin.name,
971
1090
  hook: 'stop',
972
1091
  state: 'failed',
973
1092
  order: record.order,
974
1093
  durationMs,
975
1094
  issues: [issue],
976
1095
  });
977
- this.#emitHook('stop', record.pip.name, record.order, 'failed', { durationMs, error });
978
- failures.push({ pip: record.pip.name, phase: 'stop', error });
979
- pipLogger.error('stop: failed (continuing)', {
980
- 'dot.pip.error.message': stringifyError(error),
1096
+ this.#emitHook('stop', record.plugin.name, record.order, 'failed', { durationMs, error });
1097
+ failures.push({ plugin: record.plugin.name, phase: 'stop', error });
1098
+ pluginLogger.error('stop: failed (continuing)', {
1099
+ 'dot.plugin.error.message': stringifyError(error),
981
1100
  });
982
1101
  }
983
1102
  }
@@ -1000,11 +1119,11 @@ export class DotAppImpl {
1000
1119
  }
1001
1120
  async #runDispose() {
1002
1121
  const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'dispose');
1003
- phaseLogger.debug('dispose: starting', { 'dot.app.pip.count': this.#ordered.length });
1122
+ phaseLogger.debug('dispose: starting', { 'dot.app.plugin.count': this.#ordered.length });
1004
1123
  return this.#withPhaseEmitAsync('dispose', () => withPhaseSpan({
1005
1124
  appName: this.#appName,
1006
1125
  phase: 'dispose',
1007
- pipCount: this.#ordered.length,
1126
+ pluginCount: this.#ordered.length,
1008
1127
  logger: phaseLogger,
1009
1128
  }, () => this.#runDisposeInner(phaseLogger)));
1010
1129
  }
@@ -1032,14 +1151,14 @@ export class DotAppImpl {
1032
1151
  throw new DotLifecycleError({
1033
1152
  code: DotLifecycleErrorCode.DisposeFailed,
1034
1153
  phase: 'dispose',
1035
- message: `${failures.length} pip(s) failed during stop+dispose cascade`,
1154
+ message: `${failures.length} plugin(s) failed during stop+dispose cascade`,
1036
1155
  failures,
1037
1156
  });
1038
1157
  }
1039
1158
  phaseLogger.debug('dispose: complete (cascaded from started)');
1040
1159
  return;
1041
1160
  }
1042
- // From booted/stopped/configured/defined: only dispose booted pips.
1161
+ // From booted/stopped/configured/defined: only dispose booted plugins.
1043
1162
  if (this.#state === 'failed') {
1044
1163
  // Already cleaned up at the failure site — mark disposed.
1045
1164
  this.#state = 'disposed';
@@ -1056,11 +1175,11 @@ export class DotAppImpl {
1056
1175
  throw new DotLifecycleError({
1057
1176
  code: DotLifecycleErrorCode.DisposeFailed,
1058
1177
  phase: 'dispose',
1059
- message: `${failures.length} pip(s) failed during dispose`,
1178
+ message: `${failures.length} plugin(s) failed during dispose`,
1060
1179
  failures,
1061
1180
  });
1062
1181
  }
1063
- phaseLogger.debug('dispose: complete', { 'dot.app.pip.count': this.#ordered.length });
1182
+ phaseLogger.debug('dispose: complete', { 'dot.app.plugin.count': this.#ordered.length });
1064
1183
  }
1065
1184
  async #runDisposeForRecords(records, phaseLogger) {
1066
1185
  const failures = [];
@@ -1070,7 +1189,7 @@ export class DotAppImpl {
1070
1189
  // (declaration order): dispose runs in reverse, so cleaning the handle
1071
1190
  // with a republisher would kill it before the original provider's own
1072
1191
  // dispose hook gets to use it. `records` arrives reversed, so plain
1073
- // overwrite leaves the earliest-declared pip as each handle's owner.
1192
+ // overwrite leaves the earliest-declared plugin as each handle's owner.
1074
1193
  const ownerOf = new Map();
1075
1194
  for (const record of records) {
1076
1195
  for (const value of Object.values(record.publishedServices)) {
@@ -1087,16 +1206,16 @@ export class DotAppImpl {
1087
1206
  seen.add(value);
1088
1207
  return true;
1089
1208
  });
1090
- const hasHook = record.pip.hooks.dispose !== undefined;
1209
+ const hasHook = record.plugin.hooks.dispose !== undefined;
1091
1210
  if (!hasHook && lazyPublishes.length === 0) {
1092
1211
  record.booted = false;
1093
1212
  continue;
1094
1213
  }
1095
- const pipLogger = phaseLogger.withAttribute('dot.pip.name', record.pip.name);
1214
+ const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', record.plugin.name);
1096
1215
  if (hasHook) {
1097
- await this.#runDisposeHook(record, pipLogger, failures);
1216
+ await this.#runDisposeHook(record, pluginLogger, failures);
1098
1217
  }
1099
- // Auto-dispose lazy service handles AFTER the pip's own dispose hook
1218
+ // Auto-dispose lazy service handles AFTER the plugin's own dispose hook
1100
1219
  // (the hook may still use them). Never-initialized handles no-op.
1101
1220
  for (const [serviceKey, handle] of lazyPublishes) {
1102
1221
  try {
@@ -1105,16 +1224,16 @@ export class DotAppImpl {
1105
1224
  catch (error) {
1106
1225
  const issue = makeIssue({
1107
1226
  code: DotLifecycleErrorCode.DisposeFailed,
1108
- pip: record.pip.name,
1109
- message: `lazy service "${serviceKey}" cleanup threw for pip "${record.pip.name}": ${stringifyError(error)}`,
1227
+ plugin: record.plugin.name,
1228
+ message: `lazy service "${serviceKey}" cleanup threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
1110
1229
  remediation: `Fix the error in the dispose callback passed to lazy(...) for service "${serviceKey}". Dispose continues through individual failures and reports an aggregate error.`,
1111
1230
  docsAnchor: 'dispose-failed',
1112
1231
  });
1113
1232
  record.issues.push(issue);
1114
- failures.push({ pip: record.pip.name, phase: 'dispose', error });
1115
- pipLogger.error('dispose: lazy service cleanup failed (continuing)', {
1116
- 'dot.pip.service.key': serviceKey,
1117
- 'dot.pip.error.message': stringifyError(error),
1233
+ failures.push({ plugin: record.plugin.name, phase: 'dispose', error });
1234
+ pluginLogger.error('dispose: lazy service cleanup failed (continuing)', {
1235
+ 'dot.plugin.service.key': serviceKey,
1236
+ 'dot.plugin.error.message': stringifyError(error),
1118
1237
  });
1119
1238
  }
1120
1239
  }
@@ -1124,56 +1243,56 @@ export class DotAppImpl {
1124
1243
  }
1125
1244
  return failures;
1126
1245
  }
1127
- /** Run a single pip's dispose hook with spans/events/diagnostics. */
1128
- async #runDisposeHook(record, pipLogger, failures) {
1246
+ /** Run a single plugin's dispose hook with spans/events/diagnostics. */
1247
+ async #runDisposeHook(record, pluginLogger, failures) {
1129
1248
  record.hooks.add('dispose');
1130
1249
  const ctx = this.#buildHookCtx(record, 'dispose', true);
1131
1250
  const startedAt = performance.now();
1132
- this.#emitHook('dispose', record.pip.name, record.order, 'starting');
1251
+ this.#emitHook('dispose', record.plugin.name, record.order, 'starting');
1133
1252
  try {
1134
- await this.#withHookBudget(record.pip.name, 'dispose', () => withPipHookSpan({
1253
+ await this.#withHookBudget(record.plugin.name, 'dispose', () => withPluginHookSpan({
1135
1254
  appName: this.#appName,
1136
- pipName: record.pip.name,
1137
- pipVersion: record.pip.version,
1255
+ pluginName: record.plugin.name,
1256
+ pluginVersion: record.plugin.version,
1138
1257
  hook: 'dispose',
1139
1258
  order: record.order,
1140
- logger: pipLogger,
1141
- }, () => record.pip.hooks.dispose(ctx)));
1259
+ logger: pluginLogger,
1260
+ }, () => record.plugin.hooks.dispose(ctx)));
1142
1261
  record.booted = false;
1143
1262
  const durationMs = performance.now() - startedAt;
1144
1263
  record.lifecycleDiagnostics.push({
1145
- pip: record.pip.name,
1264
+ plugin: record.plugin.name,
1146
1265
  hook: 'dispose',
1147
1266
  state: 'disposed',
1148
1267
  order: record.order,
1149
1268
  durationMs,
1150
1269
  issues: [],
1151
1270
  });
1152
- this.#emitHook('dispose', record.pip.name, record.order, 'completed', { durationMs });
1153
- pipLogger.debug('dispose: done', { 'dot.pip.duration.ms': durationMs });
1271
+ this.#emitHook('dispose', record.plugin.name, record.order, 'completed', { durationMs });
1272
+ pluginLogger.debug('dispose: done', { 'dot.plugin.duration.ms': durationMs });
1154
1273
  }
1155
1274
  catch (error) {
1156
1275
  const durationMs = performance.now() - startedAt;
1157
1276
  const issue = makeIssue({
1158
1277
  code: DotLifecycleErrorCode.DisposeFailed,
1159
- pip: record.pip.name,
1160
- message: `dispose hook threw for pip "${record.pip.name}": ${stringifyError(error)}`,
1161
- remediation: `Fix the error in the dispose() hook of "${record.pip.name}". Dispose continues through individual failures and reports an aggregate error.`,
1278
+ plugin: record.plugin.name,
1279
+ message: `dispose hook threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
1280
+ remediation: `Fix the error in the dispose() hook of "${record.plugin.name}". Dispose continues through individual failures and reports an aggregate error.`,
1162
1281
  docsAnchor: 'dispose-failed',
1163
1282
  });
1164
1283
  record.issues.push(issue);
1165
1284
  record.lifecycleDiagnostics.push({
1166
- pip: record.pip.name,
1285
+ plugin: record.plugin.name,
1167
1286
  hook: 'dispose',
1168
1287
  state: 'failed',
1169
1288
  order: record.order,
1170
1289
  durationMs,
1171
1290
  issues: [issue],
1172
1291
  });
1173
- this.#emitHook('dispose', record.pip.name, record.order, 'failed', { durationMs, error });
1174
- failures.push({ pip: record.pip.name, phase: 'dispose', error });
1175
- pipLogger.error('dispose: failed (continuing)', {
1176
- 'dot.pip.error.message': stringifyError(error),
1292
+ this.#emitHook('dispose', record.plugin.name, record.order, 'failed', { durationMs, error });
1293
+ failures.push({ plugin: record.plugin.name, phase: 'dispose', error });
1294
+ pluginLogger.error('dispose: failed (continuing)', {
1295
+ 'dot.plugin.error.message': stringifyError(error),
1177
1296
  });
1178
1297
  }
1179
1298
  }
@@ -1187,62 +1306,67 @@ export class DotAppImpl {
1187
1306
  });
1188
1307
  }
1189
1308
  #buildManifest() {
1190
- const pips = [];
1191
- const routes = [];
1309
+ const plugins = [];
1310
+ const actions = [];
1192
1311
  const services = [];
1193
1312
  const lifecycle = [];
1194
- // Edges are observed at boot time: consumer pip → the pip whose
1195
- // published wire key satisfied its need. Before boot, the per-pip
1313
+ const projections = [];
1314
+ // Edges are observed at boot time: consumer plugin the plugin whose
1315
+ // published wire key satisfied its need. Before boot, the per-plugin
1196
1316
  // `dependencies` array (wire-key strings) is the declarative view.
1197
1317
  const dependencies = this.#wiringEdges.map(e => ({ ...e }));
1198
- for (const pip of this.#ordered) {
1199
- const record = this.#records.get(pip.name);
1200
- const needWireKeys = Object.entries(pip.needs).map(([alias, witness]) => wireKeyOf(witness, alias));
1201
- pips.push({
1202
- name: pip.name,
1203
- version: pip.version,
1318
+ for (const plugin of this.#ordered) {
1319
+ const record = this.#records.get(plugin.name);
1320
+ const needWireKeys = Object.entries(plugin.needs).map(([alias, witness]) => wireKeyOf(witness, alias));
1321
+ plugins.push({
1322
+ name: plugin.name,
1323
+ version: plugin.version,
1204
1324
  dependencies: needWireKeys,
1205
1325
  provides: [...record.provides],
1206
1326
  });
1207
- routes.push(...record.routes);
1327
+ actions.push(...record.actions);
1208
1328
  services.push(...record.services);
1329
+ projections.push(...record.projections);
1209
1330
  lifecycle.push({
1210
- pip: pip.name,
1331
+ plugin: plugin.name,
1211
1332
  hooks: [...record.hooks],
1212
1333
  });
1213
1334
  }
1214
1335
  return {
1336
+ manifestVersion: 2,
1215
1337
  app: {
1216
1338
  name: this.#appName,
1217
1339
  version: this.#appVersion,
1218
1340
  },
1219
- pips,
1220
- routes,
1341
+ plugins,
1342
+ actions,
1221
1343
  services,
1222
1344
  lifecycle,
1223
1345
  dependencies,
1346
+ projections,
1224
1347
  };
1225
1348
  }
1226
1349
  #buildDiagnostics() {
1227
- const pipDiagnostics = [];
1228
- const routeDiagnostics = [];
1350
+ const pluginDiagnostics = [];
1351
+ const actionDiagnostics = [];
1229
1352
  const serviceDiagnostics = [];
1230
1353
  const lifecycleDiagnostics = [];
1231
1354
  const issues = [];
1232
- for (const pip of this.#ordered) {
1233
- const record = this.#records.get(pip.name);
1234
- // Per-pip status: failed if any issue with severity error exists, ok otherwise.
1355
+ for (const plugin of this.#ordered) {
1356
+ const record = this.#records.get(plugin.name);
1357
+ // Per-plugin status: failed if any issue with severity error exists, ok otherwise.
1235
1358
  const hasError = record.issues.some(i => i.severity === 'error');
1236
1359
  const status = hasError ? 'failed' : 'ok';
1237
- pipDiagnostics.push({
1238
- pip: pip.name,
1360
+ pluginDiagnostics.push({
1361
+ plugin: plugin.name,
1239
1362
  status,
1240
1363
  issues: [...record.issues],
1241
1364
  });
1242
- for (const route of record.routes) {
1243
- routeDiagnostics.push({
1244
- id: route.id,
1245
- pip: pip.name,
1365
+ for (const action of record.actions) {
1366
+ actionDiagnostics.push({
1367
+ id: action.id,
1368
+ plugin: plugin.name,
1369
+ binding: action.binding,
1246
1370
  status: hasError ? 'failed' : 'ok',
1247
1371
  issues: [],
1248
1372
  });
@@ -1250,7 +1374,7 @@ export class DotAppImpl {
1250
1374
  for (const svc of record.services) {
1251
1375
  serviceDiagnostics.push({
1252
1376
  service: svc.name,
1253
- pip: pip.name,
1377
+ plugin: plugin.name,
1254
1378
  status: hasError
1255
1379
  ? 'failed'
1256
1380
  : record.booted || this.#state === 'defined' || this.#state === 'configured'
@@ -1263,13 +1387,14 @@ export class DotAppImpl {
1263
1387
  issues.push(...record.issues);
1264
1388
  }
1265
1389
  return {
1390
+ snapshotVersion: 2,
1266
1391
  generatedAt: new Date().toISOString(),
1267
1392
  app: {
1268
1393
  name: this.#appName,
1269
1394
  state: this.#state,
1270
1395
  },
1271
- pips: pipDiagnostics,
1272
- routes: routeDiagnostics,
1396
+ plugins: pluginDiagnostics,
1397
+ actions: actionDiagnostics,
1273
1398
  services: serviceDiagnostics,
1274
1399
  lifecycle: lifecycleDiagnostics,
1275
1400
  issues,
@@ -1298,7 +1423,13 @@ function reverseRecords(records) {
1298
1423
  // eslint-disable-next-line unicorn/no-array-reverse -- lib target is ES2022, toReversed is ES2023.
1299
1424
  return [...records].reverse();
1300
1425
  }
1301
- /** Re-export `ServiceKind` and `RouteTransport` for the kernel's internal use. */
1426
+ function hasToDotAction(source) {
1427
+ return (typeof source === 'object' &&
1428
+ source !== null &&
1429
+ 'toDotAction' in source &&
1430
+ typeof source.toDotAction === 'function');
1431
+ }
1432
+ /** Re-export `ServiceKind` for the kernel's internal use. */
1302
1433
  export {} from '../manifest.js';
1303
- export {} from '../pip-contract.js';
1434
+ export {} from '../plugin-contract.js';
1304
1435
  //# sourceMappingURL=app-instance.js.map