@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
@@ -1,32 +1,32 @@
1
1
  /**
2
- * Test harness for unit-testing DOT pips.
2
+ * Test harness for unit-testing DOT plugins.
3
3
  *
4
- * Provides `testApp` — a convenience wrapper that lets pip authors verify
4
+ * Provides `testApp` — a convenience wrapper that lets plugin authors verify
5
5
  * lifecycle behaviour, registration, and service publishing without dragging
6
6
  * in concrete framework dependencies.
7
7
  *
8
- * NOTE: `testApp` takes an erased pip array, so the compile-time wiring
8
+ * NOTE: `testApp` takes an erased plugin array, so the compile-time wiring
9
9
  * guard does not apply here — the kernel's runtime validation (unsatisfied
10
10
  * needs, collisions) still does. Use `defineApp(...).use(...)` chains in
11
11
  * tests that should exercise the type-level guard.
12
12
  *
13
13
  * @example
14
- * import { testApp, pip } from '@arki/dot';
14
+ * import { testApp, plugin } from '@arki/dot';
15
15
  *
16
- * const myPip = pip({
16
+ * const myPlugin = plugin({
17
17
  * name: 'counter',
18
18
  * boot: () => ({ counter: { value: 0 } }),
19
19
  * });
20
20
  *
21
21
  * it('publishes a counter service', async () => {
22
- * const app = await bootTestApp<{ counter: { value: number } }>([myPip]);
22
+ * const app = await bootTestApp<{ counter: { value: number } }>([myPlugin]);
23
23
  * expect(app.services.counter.value).toBe(0);
24
24
  * await app.dispose();
25
25
  * });
26
26
  */
27
27
 
28
28
  import type { DotApp, DotAppBuilder } from './define-app.js';
29
- import type { AnyPip, EmptyShape, Pip, ServiceRecord, Token } from './pip-contract.js';
29
+ import type { AnyPlugin, EmptyShape, Plugin, ServiceRecord, Token } from './plugin-contract.js';
30
30
  import { defineApp } from './define-app.js';
31
31
 
32
32
  export type TestAppOptions = {
@@ -37,24 +37,24 @@ export type TestAppOptions = {
37
37
  };
38
38
 
39
39
  /**
40
- * Guard-free view of the builder for erased pip arrays. The runtime `use`
40
+ * Guard-free view of the builder for erased plugin arrays. The runtime `use`
41
41
  * implementation accepts the missing guard argument (it is type-level
42
42
  * only), so this seam is safe — the kernel still validates wiring at boot.
43
43
  */
44
44
  type LooseBuilder = {
45
- use(pip: AnyPip): LooseBuilder;
45
+ use(plugin: AnyPlugin): LooseBuilder;
46
46
  };
47
47
 
48
48
  /**
49
- * Build a DOT app builder pre-populated with the given pips, ready to
49
+ * Build a DOT app builder pre-populated with the given plugins, ready to
50
50
  * `.configure()`, `.boot()` or `.start()` from a test.
51
51
  */
52
52
  export function testApp<TServices extends ServiceRecord = EmptyShape>(
53
- pips: readonly AnyPip[] = [],
53
+ plugins: readonly AnyPlugin[] = [],
54
54
  options: TestAppOptions = {},
55
55
  ): DotAppBuilder<TServices> {
56
56
  let builder: unknown = defineApp(options.name ?? 'test-app', { config: options.config });
57
- for (const p of pips) {
57
+ for (const p of plugins) {
58
58
  builder = (builder as LooseBuilder).use(p);
59
59
  }
60
60
  return builder as DotAppBuilder<TServices>;
@@ -65,87 +65,87 @@ export function testApp<TServices extends ServiceRecord = EmptyShape>(
65
65
  * calling `app.dispose()` when finished.
66
66
  */
67
67
  export async function bootTestApp<TServices extends ServiceRecord = EmptyShape>(
68
- pips: readonly AnyPip[] = [],
68
+ plugins: readonly AnyPlugin[] = [],
69
69
  options: TestAppOptions = {},
70
70
  ): Promise<DotApp<TServices>> {
71
- return testApp<TServices>(pips, options).boot();
71
+ return testApp<TServices>(plugins, options).boot();
72
72
  }
73
73
 
74
74
  /**
75
- * Rest-tuple guard for {@link TestPipBuilder.boot} — the same trick the app
75
+ * Rest-tuple guard for {@link TestPluginBuilder.boot} — the same trick the app
76
76
  * builder's `UseGuard` uses. While any need is still unprovided, `boot()`
77
77
  * demands an impossible second argument, so the call site fails with
78
78
  * "Expected 2 arguments, but got 1" and the error payload names the
79
79
  * missing wire keys and their types.
80
80
  */
81
- type TestPipBootGuard<TRemaining extends ServiceRecord> = keyof TRemaining extends never
81
+ type TestPluginBootGuard<TRemaining extends ServiceRecord> = keyof TRemaining extends never
82
82
  ? readonly []
83
83
  : readonly [
84
84
  needs: {
85
- readonly 'DOT-TEST: pip needs still unprovided — call .provide() for each': {
85
+ readonly 'DOT-TEST: plugin needs still unprovided — call .provide() for each': {
86
86
  readonly [K in keyof TRemaining]: TRemaining[K];
87
87
  };
88
88
  },
89
89
  ];
90
90
 
91
91
  /**
92
- * Typed unit-test builder for a single pip — see {@link testPip}.
92
+ * Typed unit-test builder for a single plugin — see {@link testPlugin}.
93
93
  *
94
94
  * `TRemaining` tracks the wire keys not yet covered by `.provide()`;
95
95
  * `TServices` accumulates what the booted app will expose (the fakes plus
96
- * the pip's own provides).
96
+ * the plugin's own provides).
97
97
  */
98
- export type TestPipBuilder<TRemaining extends ServiceRecord, TServices extends ServiceRecord> = {
98
+ export type TestPluginBuilder<TRemaining extends ServiceRecord, TServices extends ServiceRecord> = {
99
99
  /**
100
100
  * Satisfy one need directly with a fake. Token form — the token names
101
101
  * the wire key and carries the type:
102
102
  *
103
103
  * ```ts
104
- * testPip(reports).provide(Db, fakeDb)
104
+ * testPlugin(reports).provide(Db, fakeDb)
105
105
  * ```
106
106
  */
107
107
  provide<K extends keyof TRemaining & string, T extends TRemaining[K]>(
108
108
  token: Token<T, K>,
109
109
  value: T,
110
- ): TestPipBuilder<Omit<TRemaining, K>, TServices & Readonly<Record<K, T>>>;
110
+ ): TestPluginBuilder<Omit<TRemaining, K>, TServices & Readonly<Record<K, T>>>;
111
111
  /**
112
112
  * Satisfy one need directly with a fake. Key form — for anonymous
113
113
  * `service<T>()` needs, where the wire key is the property name:
114
114
  *
115
115
  * ```ts
116
- * testPip(billing).provide('db', fakeDb)
116
+ * testPlugin(billing).provide('db', fakeDb)
117
117
  * ```
118
118
  */
119
119
  provide<K extends keyof TRemaining & string>(
120
120
  key: K,
121
121
  value: TRemaining[K],
122
- ): TestPipBuilder<Omit<TRemaining, K>, TServices & Readonly<Record<K, TRemaining[K]>>>;
123
- /** Boot the pip against the provided fakes. Compile error until every need is provided. */
124
- boot(...guard: TestPipBootGuard<TRemaining>): Promise<DotApp<TServices>>;
125
- /** Boot + start. Same guard as {@link TestPipBuilder.boot}. */
126
- start(...guard: TestPipBootGuard<TRemaining>): Promise<DotApp<TServices>>;
122
+ ): TestPluginBuilder<Omit<TRemaining, K>, TServices & Readonly<Record<K, TRemaining[K]>>>;
123
+ /** Boot the plugin against the provided fakes. Compile error until every need is provided. */
124
+ boot(...guard: TestPluginBootGuard<TRemaining>): Promise<DotApp<TServices>>;
125
+ /** Boot + start. Same guard as {@link TestPluginBuilder.boot}. */
126
+ start(...guard: TestPluginBootGuard<TRemaining>): Promise<DotApp<TServices>>;
127
127
  };
128
128
 
129
- /** Internal accumulating state for {@link testPip}. */
130
- type TestPipState = {
131
- readonly pip: AnyPip;
129
+ /** Internal accumulating state for {@link testPlugin}. */
130
+ type TestPluginState = {
131
+ readonly plugin: AnyPlugin;
132
132
  readonly fakes: Readonly<Record<string, unknown>>;
133
133
  readonly options: TestAppOptions;
134
134
  };
135
135
 
136
136
  /**
137
- * Unit-test a single pip with **typed overrides** — the compile-checked
137
+ * Unit-test a single plugin with **typed overrides** — the compile-checked
138
138
  * counterpart to {@link testApp}'s erased arrays.
139
139
  *
140
- * Each `.provide()` satisfies one of the pip's needs directly (no real
141
- * provider pip, no dependency chain) and removes it from the builder's
140
+ * Each `.provide()` satisfies one of the plugin's needs directly (no real
141
+ * provider plugin, no dependency chain) and removes it from the builder's
142
142
  * remaining-needs type. `boot()` only compiles once every need is covered,
143
143
  * and a fake of the wrong shape fails at the `.provide()` call site:
144
144
  *
145
145
  * ```ts
146
- * import { testPip } from '@arki/dot/test-harness';
146
+ * import { testPlugin } from '@arki/dot/test-harness';
147
147
  *
148
- * const app = await testPip(catalog)
148
+ * const app = await testPlugin(catalog)
149
149
  * .provide(Db, fakeDb) // token need
150
150
  * .provide('cache', fakeKv) // anonymous need — wire key is the alias
151
151
  * .boot();
@@ -157,38 +157,39 @@ type TestPipState = {
157
157
  * `service.lazy<T>()` needs accept either a plain `T` fake (the kernel
158
158
  * lifts it) or a `Lazy<T>` handle (`lazyOf(value)` is handy here).
159
159
  * Lifecycle semantics are the real kernel's — the fakes are published by a
160
- * synthetic first pip, so reverse-order teardown and lazy auto-dispose
160
+ * synthetic first plugin, so reverse-order teardown and lazy auto-dispose
161
161
  * behave exactly as in production.
162
162
  */
163
- export function testPip<TNeeds extends ServiceRecord, TProvides extends ServiceRecord>(
164
- pip: Pip<TNeeds, TProvides>,
163
+ export function testPlugin<TNeeds extends ServiceRecord, TProvides extends ServiceRecord>(
164
+ plugin: Plugin<TNeeds, TProvides>,
165
165
  options: TestAppOptions = {},
166
- ): TestPipBuilder<TNeeds, TProvides> {
166
+ ): TestPluginBuilder<TNeeds, TProvides> {
167
167
  // Erasure seam — same boundary as `makeBuilder` in define-app.ts: the
168
168
  // runtime impl is untyped, the type parameters do all the guarding.
169
- return makeTestPipBuilder({ pip: pip as AnyPip, fakes: {}, options }) as TestPipBuilder<TNeeds, TProvides>;
169
+ return makeTestPluginBuilder({ plugin: plugin as AnyPlugin, fakes: {}, options }) as TestPluginBuilder<TNeeds, TProvides>;
170
170
  }
171
171
 
172
172
  /**
173
- * Erased implementation behind {@link testPip}. Mirrors `makeBuilder` in
173
+ * Erased implementation behind {@link testPlugin}. Mirrors `makeBuilder` in
174
174
  * `define-app.ts`: the runtime is untyped, the single cast at the return
175
175
  * is the seam, and the type parameters do all the guarding.
176
176
  */
177
- function makeTestPipBuilder(state: TestPipState): TestPipBuilder<ServiceRecord, ServiceRecord> {
177
+ function makeTestPluginBuilder(state: TestPluginState): TestPluginBuilder<ServiceRecord, ServiceRecord> {
178
178
  const bootApp = async (): Promise<DotApp<ServiceRecord>> => {
179
- const pips: AnyPip[] = [];
179
+ const plugins: AnyPlugin[] = [];
180
180
  if (Object.keys(state.fakes).length > 0) {
181
181
  const fakes = { ...state.fakes };
182
- pips.push({
183
- name: `test:fakes(${state.pip.name})`,
182
+ plugins.push({
183
+ name: `test:fakes(${state.plugin.name})`,
184
184
  needs: {},
185
+ actions: [],
185
186
  renames: {},
186
187
  hooks: { boot: () => fakes },
187
188
  });
188
189
  }
189
- pips.push(state.pip);
190
- return testApp<ServiceRecord>(pips, {
191
- name: state.options.name ?? `test:${state.pip.name}`,
190
+ plugins.push(state.plugin);
191
+ return testApp<ServiceRecord>(plugins, {
192
+ name: state.options.name ?? `test:${state.plugin.name}`,
192
193
  ...(state.options.config === undefined ? {} : { config: state.options.config }),
193
194
  }).boot();
194
195
  };
@@ -196,7 +197,7 @@ function makeTestPipBuilder(state: TestPipState): TestPipBuilder<ServiceRecord,
196
197
  const impl = {
197
198
  provide(tokenOrKey: Token<unknown, string> | string, value: unknown) {
198
199
  const key = typeof tokenOrKey === 'string' ? tokenOrKey : tokenOrKey.key;
199
- return makeTestPipBuilder({ ...state, fakes: { ...state.fakes, [key]: value } });
200
+ return makeTestPluginBuilder({ ...state, fakes: { ...state.fakes, [key]: value } });
200
201
  },
201
202
  async boot(..._guard: readonly unknown[]) {
202
203
  return bootApp();
@@ -207,5 +208,5 @@ function makeTestPipBuilder(state: TestPipState): TestPipBuilder<ServiceRecord,
207
208
  return app;
208
209
  },
209
210
  };
210
- return impl as TestPipBuilder<ServiceRecord, ServiceRecord>;
211
+ return impl as TestPluginBuilder<ServiceRecord, ServiceRecord>;
211
212
  }
package/src/timeline.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ASCII waterfall renderer for DOT lifecycle diagnostics.
3
3
  *
4
- * Given a `DotDiagnosticsSnapshot`, builds a compact per-phase, per-pip
4
+ * Given a `DotDiagnosticsSnapshot`, builds a compact per-phase, per-plugin
5
5
  * duration chart suitable for printing in a terminal or embedding in a
6
6
  * `dot doctor` text report. No colour, no Unicode beyond `█` so the
7
7
  * output stays clean in log aggregators and CI.
@@ -38,7 +38,7 @@ export type RenderTimelineOptions = {
38
38
  /** Maximum bar width in characters (default `50`). */
39
39
  readonly barWidth?: number;
40
40
  /**
41
- * When `true`, also emit any pip's diagnostic `issues[]` underneath
41
+ * When `true`, also emit any plugin's diagnostic `issues[]` underneath
42
42
  * the bar so failure reasons are visible in the timeline view.
43
43
  * Default `true`.
44
44
  */
@@ -78,8 +78,8 @@ export function renderTimeline(snapshot: DotDiagnosticsSnapshot, opts: RenderTim
78
78
  lines.push(title);
79
79
  lines.push('─'.repeat(title.length));
80
80
 
81
- // Layout: 2-space indent, 10-char pip column, 8-char duration column, then bar.
82
- const PIP_COL = 12;
81
+ // Layout: 2-space indent, 10-char plugin column, 8-char duration column, then bar.
82
+ const PLUGIN_COL = 12;
83
83
  const DUR_COL = 8;
84
84
  const indent = ' ';
85
85
 
@@ -94,9 +94,9 @@ export function renderTimeline(snapshot: DotDiagnosticsSnapshot, opts: RenderTim
94
94
  const durationMs = entry.durationMs ?? 0;
95
95
  const bar = makeBar(durationMs, maxDuration, barWidth);
96
96
  const failedMark = entry.state === 'failed' ? ' ✗' : '';
97
- const pipCol = entry.pip.padEnd(PIP_COL);
97
+ const pluginCol = entry.plugin.padEnd(PLUGIN_COL);
98
98
  const durCol = formatDuration(durationMs).padStart(DUR_COL);
99
- lines.push(`${indent}${pipCol}${durCol} ${bar}${failedMark}`);
99
+ lines.push(`${indent}${pluginCol}${durCol} ${bar}${failedMark}`);
100
100
  if (showIssues && entry.issues.length > 0) {
101
101
  for (const issue of entry.issues) {
102
102
  lines.push(`${indent} └─ ${issue.code}: ${issue.message}`);
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What this app is
4
4
 
5
- A DOT app composed via `defineApp(...).use(pip).boot()`. The runtime
5
+ A DOT app composed via `defineApp(...).use(plugin).boot()`. The runtime
6
6
  graph is statically inspectable via `app.manifest` and runtime-inspectable
7
7
  via `app.diagnostics`.
8
8
 
@@ -26,7 +26,7 @@ command exits zero.
26
26
  - Env schema lives at `src/env.ts` — keep it narrow; one entry per env
27
27
  key the app actually reads.
28
28
  - Tests live under `tests/` and import the app via the `src/` barrel.
29
- - Add new pips by wiring `.use(pip(options))` between
29
+ - Add new plugins by wiring `.use(plugin(options))` between
30
30
  `defineApp('{{name}}')` and `.boot()` in `src/app.ts`.
31
31
 
32
32
  ## Public / private boundaries
@@ -36,8 +36,8 @@ command exits zero.
36
36
  app reads belong here.
37
37
  - Don't reach into `@arki/dot` internals (anything under
38
38
  `@arki/dot/kernel`, `@arki/dot/legacy`, or `dist/` subpaths). Use the
39
- public `defineApp` / `pip` API from `@arki/dot` and the
40
- `pip` / `service` / `rename` symbols from `@arki/dot/pip`.
39
+ public `defineApp` / `plugin` API from `@arki/dot` and the
40
+ `plugin` / `service` / `rename` symbols from `@arki/dot/plugin`.
41
41
 
42
42
  ## DOT artifact locations
43
43
 
@@ -47,14 +47,14 @@ command exits zero.
47
47
  `started` -> `stopped` -> `disposed`).
48
48
  - Errors carry stable `code`, `severity`, `remediation`, `docsUrl`.
49
49
 
50
- ## Adding a pip
50
+ ## Adding a plugin
51
51
 
52
52
  1. Install the adapter package: `{{pkgManager}} add @arki/<pkg>`.
53
- 2. Import its DOT adapter: `import { <pip> } from '@arki/<pkg>/dot';`.
53
+ 2. Import its DOT adapter: `import { <plugin> } from '@arki/<pkg>/dot';`.
54
54
  3. Chain it into the builder before `.boot()`:
55
- `.use(<pip>(options))`.
56
- 4. Verify with `bunx dot explain --json` — the new pip should appear
57
- under `.data.pips`.
55
+ `.use(<plugin>(options))`.
56
+ 4. Verify with `bunx dot explain --json` — the new plugin should appear
57
+ under `.data.plugins`.
58
58
 
59
59
  ## Diagnostics triage
60
60
 
@@ -17,7 +17,7 @@ bunx dot explain --json
17
17
  bunx dot doctor --json
18
18
  ```
19
19
 
20
- `dot explain` prints the static graph (pips, services, routes, dependencies,
20
+ `dot explain` prints the static graph (plugins, services, routes, dependencies,
21
21
  lifecycle hooks). `dot doctor` boots the app and prints the runtime
22
22
  diagnostics envelope (the same five arrays, plus issues).
23
23
 
@@ -17,7 +17,7 @@ test('app boots and exposes env service', () => {
17
17
 
18
18
  test('diagnostics has all 5 arrays', () => {
19
19
  const { diagnostics } = booted;
20
- expect(diagnostics.pips).toBeDefined();
20
+ expect(diagnostics.plugins).toBeDefined();
21
21
  expect(diagnostics.routes).toBeDefined();
22
22
  expect(diagnostics.services).toBeDefined();
23
23
  expect(diagnostics.lifecycle).toBeDefined();
@@ -1,147 +0,0 @@
1
- /**
2
- * Renderer for `dot explain --openapi`.
3
- *
4
- * Emits an OpenAPI 3.1 document straight from `manifest.routes` — no boot
5
- * required. Adapters (e.g. `@arki/http`'s `registerRoutes`) convert their
6
- * validation schemas to JSON Schema at `configure` time and register them
7
- * via `ctx.registerRoute`, so the manifest already carries everything the
8
- * document needs: methods, paths, per-location input schemas, output
9
- * schemas, and the `streaming` flag for `text/event-stream` responses.
10
- *
11
- * Deterministic: paths and operations follow manifest registration order
12
- * (which follows pip declaration order). Only `transport: 'http'` routes
13
- * are rendered — rpc mounts have their own schema story.
14
- */
15
-
16
- import type { DotAppManifest, RouteManifest } from '../manifest.js';
17
- import type { DotCliEnvelope, RenderOptions } from './render-explain.js';
18
-
19
- /** Envelope payload for OpenAPI output. */
20
- export type OpenApiData = {
21
- format: 'openapi';
22
- /** OpenAPI 3.1 document. */
23
- document: Record<string, unknown>;
24
- };
25
-
26
- function openApiPath(path: string): string {
27
- return path.replaceAll(/:(\w+)/g, '{$1}');
28
- }
29
-
30
- function pathParamNames(path: string): readonly string[] {
31
- return [...path.matchAll(/:(\w+)/g)]
32
- .map(match => match[1])
33
- .filter((name): name is string => name !== undefined);
34
- }
35
-
36
- function buildOperation(route: RouteManifest): Record<string, unknown> {
37
- const parameters: Record<string, unknown>[] = pathParamNames(route.path ?? '').map(name => ({
38
- name,
39
- in: 'path',
40
- required: true,
41
- schema: { type: 'string' },
42
- }));
43
-
44
- const querySchema = route.input?.query;
45
- if (querySchema !== undefined) {
46
- const properties = (querySchema['properties'] ?? {}) as Readonly<Record<string, unknown>>;
47
- const required = new Set(querySchema['required'] as readonly string[] | undefined);
48
- for (const [name, propertySchema] of Object.entries(properties)) {
49
- parameters.push({ name, in: 'query', required: required.has(name), schema: propertySchema });
50
- }
51
- }
52
-
53
- const operation: Record<string, unknown> = {
54
- operationId: route.id,
55
- tags: [route.pip],
56
- };
57
- if (route.description !== undefined) operation['summary'] = route.description;
58
- if (parameters.length > 0) operation['parameters'] = parameters;
59
-
60
- const bodySchema = route.input?.body;
61
- if (bodySchema !== undefined) {
62
- operation['requestBody'] = {
63
- required: true,
64
- content: { 'application/json': { schema: bodySchema } },
65
- };
66
- }
67
-
68
- if (route.streaming === true) {
69
- operation['responses'] = {
70
- '200': {
71
- description: 'event stream',
72
- content: {
73
- 'text/event-stream': route.output === undefined ? {} : { schema: route.output },
74
- },
75
- },
76
- };
77
- } else if (route.output === undefined) {
78
- operation['responses'] = { '200': { description: 'success' } };
79
- } else {
80
- operation['responses'] = {
81
- '200': {
82
- description: 'success',
83
- content: { 'application/json': { schema: route.output } },
84
- },
85
- };
86
- }
87
-
88
- return operation;
89
- }
90
-
91
- /**
92
- * Build an OpenAPI 3.1 document from a manifest. First registration wins
93
- * on a method+path collision — collisions are an app bug the serving
94
- * adapter rejects at boot; the document renderer stays total.
95
- */
96
- export function buildOpenApiDocument(manifest: DotAppManifest): Record<string, unknown> {
97
- const paths: Record<string, Record<string, unknown>> = {};
98
- for (const route of manifest.routes) {
99
- if (route.transport !== 'http') continue;
100
- if (route.path === undefined) continue;
101
- const path = openApiPath(route.path);
102
- const method = (route.method ?? 'GET').toLowerCase();
103
- const entry = (paths[path] ??= {});
104
- if (entry[method] !== undefined) continue;
105
- entry[method] = buildOperation(route);
106
- }
107
- return {
108
- openapi: '3.1.0',
109
- info: {
110
- title: manifest.app.name,
111
- version: manifest.app.version ?? '0.0.0',
112
- },
113
- paths,
114
- };
115
- }
116
-
117
- /**
118
- * Render the OpenAPI output. Plain mode prints the document as JSON
119
- * (pipe it into a file or a swagger viewer); `--json` wraps it in the
120
- * standard CLI envelope under `data.document`.
121
- */
122
- export function renderOpenApi(
123
- source: { manifest: DotAppManifest; command: 'explain' },
124
- opts: RenderOptions,
125
- ): DotCliEnvelope<OpenApiData> {
126
- const document = buildOpenApiDocument(source.manifest);
127
- const nowFactory = opts.now ?? (() => new Date());
128
- const envelope: DotCliEnvelope<OpenApiData> = {
129
- status: 'success',
130
- command: source.command,
131
- generatedAt: nowFactory().toISOString(),
132
- data: { format: 'openapi', document },
133
- errors: [],
134
- };
135
-
136
- const out =
137
- opts.out ??
138
- ((line: string) => {
139
- process.stdout.write(line);
140
- });
141
- if (opts.json) {
142
- out(`${JSON.stringify(envelope, null, 2)}\n`);
143
- } else {
144
- out(`${JSON.stringify(document, null, 2)}\n`);
145
- }
146
- return envelope;
147
- }