@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
@@ -70,14 +70,14 @@ function renderTextManifest(manifest: DotAppManifest): string {
70
70
  lines.push('='.repeat(title.length));
71
71
  lines.push('');
72
72
 
73
- // Pips
74
- lines.push(`Pips (${manifest.pips.length})`);
75
- if (manifest.pips.length === 0) {
73
+ // Plugins
74
+ lines.push(`Plugins (${manifest.plugins.length})`);
75
+ if (manifest.plugins.length === 0) {
76
76
  lines.push(' (none)');
77
77
  } else {
78
- const widthName = Math.max(6, ...manifest.pips.map(p => p.name.length));
78
+ const widthName = Math.max(6, ...manifest.plugins.map(p => p.name.length));
79
79
  lines.push(` ${pad('NAME', widthName)} VERSION DEPENDENCIES`);
80
- for (const p of manifest.pips) {
80
+ for (const p of manifest.plugins) {
81
81
  const version = p.version ?? '-';
82
82
  const deps = p.dependencies.length > 0 ? p.dependencies.join(', ') : '-';
83
83
  lines.push(` ${pad(p.name, widthName)} ${pad(version, 7)} ${deps}`);
@@ -94,23 +94,41 @@ function renderTextManifest(manifest: DotAppManifest): string {
94
94
  const widthKind = Math.max(4, ...manifest.services.map(s => s.kind.length));
95
95
  lines.push(` ${pad('NAME', widthName)} ${pad('KIND', widthKind)} PLUGIN`);
96
96
  for (const s of manifest.services) {
97
- lines.push(` ${pad(s.name, widthName)} ${pad(s.kind, widthKind)} ${s.pip}`);
97
+ lines.push(` ${pad(s.name, widthName)} ${pad(s.kind, widthKind)} ${s.plugin}`);
98
98
  }
99
99
  }
100
100
  lines.push('');
101
101
 
102
- // Routes
103
- lines.push(`Routes (${manifest.routes.length})`);
104
- if (manifest.routes.length === 0) {
102
+ // Actions
103
+ lines.push(`Actions (${manifest.actions.length})`);
104
+ if (manifest.actions.length === 0) {
105
105
  lines.push(' (none)');
106
106
  } else {
107
- const widthId = Math.max(2, ...manifest.routes.map(r => r.id.length));
108
- const widthMethod = Math.max(6, ...manifest.routes.map(r => (r.method ?? '-').length));
109
- lines.push(` ${pad('ID', widthId)} ${pad('METHOD', widthMethod)} PATH/TRANSPORT PLUGIN`);
110
- for (const r of manifest.routes) {
111
- const method = r.method ?? '-';
112
- const target = r.path ?? `(${r.transport})`;
113
- lines.push(` ${pad(r.id, widthId)} ${pad(method, widthMethod)} ${pad(target, 14)} ${r.pip}`);
107
+ const widthId = Math.max(2, ...manifest.actions.map(action => action.id.length));
108
+ const widthBinding = Math.max(7, ...manifest.actions.map(action => action.binding.length));
109
+ const widthAddress = Math.max(7, ...manifest.actions.map(action => (action.address ?? '-').length));
110
+ lines.push(` ${pad('ID', widthId)} ${pad('BINDING', widthBinding)} DIR ${pad('ADDRESS', widthAddress)} PLUGIN`);
111
+ for (const action of manifest.actions) {
112
+ lines.push(
113
+ ` ${pad(action.id, widthId)} ${pad(action.binding, widthBinding)} ${pad(action.direction, 3)} ${pad(action.address ?? '-', widthAddress)} ${action.plugin}`,
114
+ );
115
+ }
116
+ }
117
+ lines.push('');
118
+
119
+ // Projections
120
+ lines.push(`Projections (${manifest.projections.length})`);
121
+ if (manifest.projections.length === 0) {
122
+ lines.push(' (none)');
123
+ } else {
124
+ const widthFormat = Math.max(6, ...manifest.projections.map(projection => projection.format.length));
125
+ const widthBinding = Math.max(7, ...manifest.projections.map(projection => projection.binding.length));
126
+ const widthModule = Math.max(6, ...manifest.projections.map(projection => projection.module.length));
127
+ lines.push(` ${pad('FORMAT', widthFormat)} ${pad('BINDING', widthBinding)} ${pad('MODULE', widthModule)} PLUGIN`);
128
+ for (const projection of manifest.projections) {
129
+ lines.push(
130
+ ` ${pad(projection.format, widthFormat)} ${pad(projection.binding, widthBinding)} ${pad(projection.module, widthModule)} ${projection.plugin}`,
131
+ );
114
132
  }
115
133
  }
116
134
  lines.push('');
@@ -131,11 +149,11 @@ function renderTextManifest(manifest: DotAppManifest): string {
131
149
  if (manifest.lifecycle.length === 0) {
132
150
  lines.push(' (none)');
133
151
  } else {
134
- const widthPip = Math.max(6, ...manifest.lifecycle.map(l => l.pip.length));
135
- lines.push(` ${pad('PLUGIN', widthPip)} HOOKS`);
152
+ const widthPlugin = Math.max(6, ...manifest.lifecycle.map(l => l.plugin.length));
153
+ lines.push(` ${pad('PLUGIN', widthPlugin)} HOOKS`);
136
154
  for (const l of manifest.lifecycle) {
137
155
  const hooks = l.hooks.length > 0 ? l.hooks.join(', ') : '-';
138
- lines.push(` ${pad(l.pip, widthPip)} ${hooks}`);
156
+ lines.push(` ${pad(l.plugin, widthPlugin)} ${hooks}`);
139
157
  }
140
158
  }
141
159
  lines.push('');
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Renderer for `dot explain --graph` / `dot doctor --graph`.
3
3
  *
4
- * Emits the app's pip graph as [Mermaid](https://mermaid.js.org) `flowchart`
4
+ * Emits the app's plugin graph as [Mermaid](https://mermaid.js.org) `flowchart`
5
5
  * source — paste-able into GitHub markdown, docs, and mermaid.live.
6
6
  *
7
- * The nodes are the pips in declaration order (which IS boot order in v2 —
7
+ * The nodes are the plugins in declaration order (which IS boot order in v2 —
8
8
  * the numbering makes that visible); the edges are the manifest's
9
- * **observed** dependency edges, recorded by the kernel when a pip's need
9
+ * **observed** dependency edges, recorded by the kernel when a plugin's need
10
10
  * was satisfied during boot. `explain` never boots, so its graph shows
11
11
  * declaration order with whatever edges configure-time metadata declared;
12
12
  * `doctor` boots, so its graph shows the real wiring.
@@ -22,7 +22,7 @@ export type GraphData = {
22
22
  source: string;
23
23
  };
24
24
 
25
- /** Escape a pip name for use inside a quoted Mermaid node label. */
25
+ /** Escape a plugin name for use inside a quoted Mermaid node label. */
26
26
  function escapeLabel(name: string): string {
27
27
  return name.replaceAll('"', '#quot;');
28
28
  }
@@ -33,20 +33,20 @@ function escapeLabel(name: string): string {
33
33
  */
34
34
  export function buildMermaidGraph(manifest: DotAppManifest): string {
35
35
  const lines: string[] = ['flowchart TD'];
36
- const idByPip = new Map<string, string>();
36
+ const idByPlugin = new Map<string, string>();
37
37
 
38
- for (const [index, pip] of manifest.pips.entries()) {
38
+ for (const [index, plugin] of manifest.plugins.entries()) {
39
39
  const id = `p${index.toString()}`;
40
- idByPip.set(pip.name, id);
40
+ idByPlugin.set(plugin.name, id);
41
41
  const order = (index + 1).toString();
42
- const version = pip.version === undefined ? '' : `@${pip.version}`;
43
- lines.push(` ${id}["${order} · ${escapeLabel(pip.name)}${escapeLabel(version)}"]`);
42
+ const version = plugin.version === undefined ? '' : `@${plugin.version}`;
43
+ lines.push(` ${id}["${order} · ${escapeLabel(plugin.name)}${escapeLabel(version)}"]`);
44
44
  }
45
45
 
46
46
  for (const edge of manifest.dependencies) {
47
- const from = idByPip.get(edge.from);
48
- const to = idByPip.get(edge.to);
49
- // Edges referencing unknown pips would be a kernel bug — skip rather
47
+ const from = idByPlugin.get(edge.from);
48
+ const to = idByPlugin.get(edge.to);
49
+ // Edges referencing unknown plugins would be a kernel bug — skip rather
50
50
  // than emit invalid Mermaid.
51
51
  if (from === undefined || to === undefined) continue;
52
52
  lines.push(` ${from} -->|${edge.kind}| ${to}`);
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Renderer for `dot explain --as <format>`.
3
+ *
4
+ * The kernel owns selection, app-file-relative module resolution, import, and
5
+ * envelope printing. The adapter owns the projection module and document
6
+ * vocabulary.
7
+ */
8
+
9
+ import { createRequire } from 'node:module';
10
+ import { dirname, isAbsolute } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ import type { DotAppManifest, JsonValue, ProjectionManifest } from '../manifest.js';
14
+ import type { DotCliEnvelope, RenderOptions } from './render-explain.js';
15
+ import { DotCliError, DotCliErrorCode } from './error-codes.js';
16
+
17
+ export type ProjectionData = {
18
+ format: string;
19
+ document: JsonValue;
20
+ };
21
+
22
+ type ProjectionModule = {
23
+ project(manifest: DotAppManifest): unknown | Promise<unknown>;
24
+ };
25
+
26
+ export type ProjectionSource = {
27
+ manifest: DotAppManifest;
28
+ command: 'explain';
29
+ format: string;
30
+ /** Absolute app file path; projection specifiers resolve relative to it. */
31
+ appFilePath: string;
32
+ /** Per-invocation debug override for the selected projection module. */
33
+ module?: string;
34
+ };
35
+
36
+ const defaultOut = (line: string) => {
37
+ process.stdout.write(line);
38
+ };
39
+
40
+ function nowIso(opts: RenderOptions): string {
41
+ const factory = opts.now ?? (() => new Date());
42
+ return factory().toISOString();
43
+ }
44
+
45
+ function isJsonPrimitive(value: unknown): value is string | number | boolean | null {
46
+ return value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number';
47
+ }
48
+
49
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
50
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
51
+ const prototype = Object.getPrototypeOf(value) as unknown;
52
+ return prototype === Object.prototype || prototype === null;
53
+ }
54
+
55
+ function isJsonValue(value: unknown): value is JsonValue {
56
+ if (isJsonPrimitive(value)) return typeof value !== 'number' || Number.isFinite(value);
57
+ if (Array.isArray(value)) return value.every(isJsonValue);
58
+ if (!isPlainObject(value)) return false;
59
+ return Object.values(value).every(isJsonValue);
60
+ }
61
+
62
+ function jsonDeepEqual(left: unknown, right: unknown): boolean {
63
+ if (isJsonPrimitive(left) || isJsonPrimitive(right)) return Object.is(left, right);
64
+ if (Array.isArray(left) || Array.isArray(right)) {
65
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
66
+ return left.every((value, index) => jsonDeepEqual(value, right[index]));
67
+ }
68
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
69
+ const leftEntries = Object.entries(left);
70
+ const rightEntries = Object.entries(right);
71
+ if (leftEntries.length !== rightEntries.length) return false;
72
+ for (const [key, value] of leftEntries) {
73
+ if (!Object.hasOwn(right, key) || !jsonDeepEqual(value, right[key])) return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ function toProjectionJsonValue(value: unknown): JsonValue {
79
+ let serialized: string;
80
+ try {
81
+ serialized = JSON.stringify(value);
82
+ } catch (error) {
83
+ throw new TypeError('Projection output must be JSON-serializable.', { cause: error });
84
+ }
85
+ if (serialized === undefined) {
86
+ throw new TypeError('Projection output must be JSON-serializable.');
87
+ }
88
+ const parsed = JSON.parse(serialized) as unknown;
89
+ if (!isJsonValue(parsed) || !jsonDeepEqual(value, parsed)) {
90
+ throw new TypeError('Projection output must survive a JSON round trip without lossy coercions.');
91
+ }
92
+ return parsed;
93
+ }
94
+
95
+ function selectProjection(manifest: DotAppManifest, format: string): ProjectionManifest {
96
+ const projections = manifest.projections.filter(projection => projection.format === format);
97
+ if (projections.length === 0) {
98
+ const formats = [...new Set(manifest.projections.map(projection => projection.format))];
99
+ throw new DotCliError({
100
+ code: DotCliErrorCode.ProjectionNotFound,
101
+ message: `No projection is registered for format "${format}".`,
102
+ remediation:
103
+ formats.length === 0
104
+ ? 'Register an adapter that contributes this projection format, or remove --as.'
105
+ : `Use one of the registered formats (${formats.join(', ')}), or mount the adapter that provides "${format}".`,
106
+ metadata: { format, registeredFormats: formats },
107
+ });
108
+ }
109
+
110
+ const modules = new Set(projections.map(projection => projection.module));
111
+ if (modules.size > 1) {
112
+ throw new DotCliError({
113
+ code: DotCliErrorCode.ProjectionConflict,
114
+ message: `Multiple projection modules are registered for format "${format}".`,
115
+ remediation:
116
+ 'Only one module may own a projection format. Remove one registration, or make both plugins register the same module specifier.',
117
+ metadata: {
118
+ format,
119
+ projections: projections.map(projection => ({ plugin: projection.plugin, module: projection.module })),
120
+ },
121
+ });
122
+ }
123
+
124
+ return projections[0]!;
125
+ }
126
+
127
+ function getBunResolver(): { resolveSync(specifier: string, from: string): string } | undefined {
128
+ const candidate = (globalThis as { Bun?: unknown }).Bun;
129
+ if (typeof candidate !== 'object' || candidate === null) return undefined;
130
+ const resolveSync = (candidate as { resolveSync?: unknown }).resolveSync;
131
+ if (typeof resolveSync !== 'function') return undefined;
132
+ return { resolveSync: (specifier, from) => resolveSync.call(candidate, specifier, from) as string };
133
+ }
134
+
135
+ function importSpecifierFor(resolved: string): string {
136
+ if (resolved.startsWith('file:')) return resolved;
137
+ if (isAbsolute(resolved)) return pathToFileURL(resolved).href;
138
+ return resolved;
139
+ }
140
+
141
+ function resolveProjectionSpecifier(specifier: string, appFilePath: string): string {
142
+ const fromDir = dirname(appFilePath);
143
+ const bun = getBunResolver();
144
+ if (bun !== undefined) return bun.resolveSync(specifier, fromDir);
145
+ return createRequire(appFilePath).resolve(specifier);
146
+ }
147
+
148
+ async function loadProjectionModule(
149
+ specifier: string,
150
+ appFilePath: string,
151
+ registeringPlugin: string,
152
+ ): Promise<{ module: ProjectionModule; resolved: string }> {
153
+ let resolved: string;
154
+ try {
155
+ resolved = resolveProjectionSpecifier(specifier, appFilePath);
156
+ } catch (error) {
157
+ throw new DotCliError({
158
+ code: DotCliErrorCode.ProjectionImportFailed,
159
+ message: `Failed to resolve projection module "${specifier}".`,
160
+ remediation:
161
+ 'Check that the projection module is installed and that package subpath exports include a default condition. Use --module <specifier> to debug with a replacement module.',
162
+ metadata: { specifier, appFilePath, registeringPlugin },
163
+ cause: error,
164
+ });
165
+ }
166
+
167
+ let imported: unknown;
168
+ try {
169
+ imported = await import(importSpecifierFor(resolved));
170
+ } catch (error) {
171
+ throw new DotCliError({
172
+ code: DotCliErrorCode.ProjectionImportFailed,
173
+ message: `Failed to import projection module "${specifier}".`,
174
+ remediation:
175
+ 'Fix the projection module export, or use --module <specifier> to debug with a replacement module.',
176
+ metadata: { specifier, resolved, registeringPlugin },
177
+ cause: error,
178
+ });
179
+ }
180
+
181
+ if (typeof imported !== 'object' || imported === null || typeof (imported as { project?: unknown }).project !== 'function') {
182
+ throw new DotCliError({
183
+ code: DotCliErrorCode.ProjectionImportFailed,
184
+ message: `Projection module "${specifier}" does not export project(manifest).`,
185
+ remediation:
186
+ 'Export a named `project(manifest)` function from the projection module. Use --module <specifier> to test a corrected module.',
187
+ metadata: { specifier, resolved, registeringPlugin },
188
+ });
189
+ }
190
+
191
+ return { module: imported as ProjectionModule, resolved };
192
+ }
193
+
194
+ /**
195
+ * Render an adapter-owned projection. Plain mode prints strings verbatim and
196
+ * JSON values as pretty JSON; JSON mode wraps the document in the standard
197
+ * CLI envelope.
198
+ */
199
+ export async function renderProjection(
200
+ source: ProjectionSource,
201
+ opts: RenderOptions,
202
+ ): Promise<DotCliEnvelope<ProjectionData>> {
203
+ const projection = selectProjection(source.manifest, source.format);
204
+ const specifier = source.module ?? projection.module;
205
+ const { module } = await loadProjectionModule(specifier, source.appFilePath, projection.plugin);
206
+
207
+ let document: JsonValue;
208
+ try {
209
+ document = toProjectionJsonValue(await module.project(source.manifest));
210
+ } catch (error) {
211
+ throw new DotCliError({
212
+ code: DotCliErrorCode.ProjectionExecutionFailed,
213
+ message: `Projection "${source.format}" failed while rendering.`,
214
+ remediation:
215
+ 'Fix the projection project(manifest) function so it is pure and returns JSON-round-trip-safe output.',
216
+ metadata: { format: source.format, specifier, registeringPlugin: projection.plugin },
217
+ cause: error,
218
+ });
219
+ }
220
+
221
+ const envelope: DotCliEnvelope<ProjectionData> = {
222
+ status: 'success',
223
+ command: source.command,
224
+ generatedAt: nowIso(opts),
225
+ data: { format: source.format, document },
226
+ errors: [],
227
+ };
228
+
229
+ const out = opts.out ?? defaultOut;
230
+ if (opts.json) {
231
+ out(`${JSON.stringify(envelope, null, 2)}\n`);
232
+ } else if (typeof document === 'string') {
233
+ out(`${document}\n`);
234
+ } else {
235
+ out(`${JSON.stringify(document, null, 2)}\n`);
236
+ }
237
+
238
+ return envelope;
239
+ }
package/src/define-app.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Public entry point for the DOT kernel (v2).
3
3
  *
4
- * `defineApp(name)` returns a `DotAppBuilder` that accumulates pips via
5
- * `.use(pip)`, then transitions through the 5-hook lifecycle:
4
+ * `defineApp(name)` returns a `DotAppBuilder` that accumulates plugins via
5
+ * `.use(plugin)`, then transitions through the 5-hook lifecycle:
6
6
  *
7
7
  * defineApp -> use* -> configure() -> boot() -> start() -> stop() -> dispose()
8
8
  *
9
- * `.use()` is compile-time guarded: a pip whose `needs` are not satisfied
9
+ * `.use()` is compile-time guarded: a plugin whose `needs` are not satisfied
10
10
  * by services provided so far — or whose provides collide with existing
11
11
  * wire keys — fails to typecheck at the call site ("Expected 2 arguments,
12
12
  * but got 1", with the diagnostic embedded in the expected second
@@ -23,7 +23,7 @@ import type { DotDiagnosticsSnapshot } from './diagnostics.js';
23
23
  import type { DotLifecycleObserver } from './lifecycle-observer.js';
24
24
  import type { DotLifecycleState } from './lifecycle.js';
25
25
  import type { DotAppManifest } from './manifest.js';
26
- import type { AnyPip, EmptyShape, Pip, ServiceRecord } from './pip-contract.js';
26
+ import type { AnyPlugin, EmptyShape, Plugin, ServiceRecord } from './plugin-contract.js';
27
27
  import { DotAppImpl } from './kernel/app-instance.js';
28
28
  import { renderTimeline } from './timeline.js';
29
29
 
@@ -43,26 +43,26 @@ type MismatchedKeys<TAvail, TNeeds> = {
43
43
  };
44
44
 
45
45
  type NeedsError<TAvail extends ServiceRecord, TNeeds extends ServiceRecord> = {
46
- readonly 'DOT: pip needs services no earlier .use() provides': {
46
+ readonly 'DOT: plugin needs services no earlier .use() provides': {
47
47
  readonly missing: MissingKeys<TAvail, TNeeds>;
48
48
  readonly mismatched: MismatchedKeys<TAvail, TNeeds>;
49
49
  };
50
50
  };
51
51
 
52
52
  type CollisionError<TAvail, TProvides> = {
53
- readonly 'DOT: pip provides wire keys already provided (use rename())': keyof TAvail & keyof TProvides;
53
+ readonly 'DOT: plugin provides wire keys already provided (use rename())': keyof TAvail & keyof TProvides;
54
54
  };
55
55
 
56
56
  /**
57
- * A pip whose `boot` always throws infers `TProvides = never`, which would
57
+ * A plugin whose `boot` always throws infers `TProvides = never`, which would
58
58
  * poison both the collision check (`keyof never` matches everything) and
59
- * the accumulated record (`TAvail & never` = `never`). Such a pip provides
59
+ * the accumulated record (`TAvail & never` = `never`). Such a plugin provides
60
60
  * nothing — normalize to the empty shape.
61
61
  */
62
- type NormalizeProvides<TP extends ServiceRecord> = [TP] extends [never] ? EmptyShape : TP;
62
+ export type NormalizeProvides<TP extends ServiceRecord> = [TP] extends [never] ? EmptyShape : TP;
63
63
 
64
64
  /**
65
- * Rest-tuple guard. Satisfied → `[]` (call `.use(pip)` with one argument).
65
+ * Rest-tuple guard. Satisfied → `[]` (call `.use(plugin)` with one argument).
66
66
  * Violated → a required second argument of an unconstructible error type,
67
67
  * so the call site fails with the diagnostic embedded in the expected type.
68
68
  */
@@ -76,6 +76,53 @@ export type UseGuard<
76
76
  : [error: CollisionError<TAvail, TProvides>]
77
77
  : [error: NeedsError<TAvail, TNeeds>];
78
78
 
79
+ type UseAllState<TAvail extends ServiceRecord, TPlugins extends readonly AnyPlugin[]> = TPlugins extends readonly [
80
+ infer Head,
81
+ ...infer Tail,
82
+ ]
83
+ ? Head extends Plugin<infer TNeeds, infer TProvides>
84
+ ? UseGuard<TAvail, TNeeds, NormalizeProvides<TProvides>> extends []
85
+ ? UseAllState<TAvail & NormalizeProvides<TProvides>, Extract<Tail, readonly AnyPlugin[]>>
86
+ : {
87
+ readonly avail: TAvail;
88
+ readonly error: {
89
+ readonly 'DOT: useAll tuple contains a plugin that cannot be used at this position': UseGuard<
90
+ TAvail,
91
+ TNeeds,
92
+ NormalizeProvides<TProvides>
93
+ >[0];
94
+ };
95
+ }
96
+ : {
97
+ readonly avail: TAvail;
98
+ readonly error: {
99
+ readonly 'DOT: useAll accepts only plugins': Head;
100
+ };
101
+ }
102
+ : {
103
+ readonly avail: TAvail;
104
+ readonly error: never;
105
+ };
106
+
107
+ export type UseAllAvail<TAvail extends ServiceRecord, TPlugins extends readonly AnyPlugin[]> = UseAllState<
108
+ TAvail,
109
+ TPlugins
110
+ >['avail'];
111
+
112
+ type UseAllTupleGuard<TPlugins extends readonly AnyPlugin[]> = number extends TPlugins['length']
113
+ ? [
114
+ error: {
115
+ readonly 'DOT: useAll requires a tuple; add `as const` or pass plugins directly in declaration order': TPlugins;
116
+ },
117
+ ]
118
+ : [];
119
+
120
+ export type UseAllGuard<TAvail extends ServiceRecord, TPlugins extends readonly AnyPlugin[]> = [
121
+ UseAllState<TAvail, TPlugins>['error'],
122
+ ] extends [never]
123
+ ? UseAllTupleGuard<TPlugins>
124
+ : [error: UseAllState<TAvail, TPlugins>['error']];
125
+
79
126
  /* ------------------------------------------------------------------ */
80
127
  /* Public app surface */
81
128
  /* ------------------------------------------------------------------ */
@@ -90,7 +137,7 @@ export type DotApp<TServices extends ServiceRecord> = {
90
137
  /** Current lifecycle state. */
91
138
  readonly state: DotLifecycleState;
92
139
  /**
93
- * Services published by booted pips, keyed by wire key.
140
+ * Services published by booted plugins, keyed by wire key.
94
141
  * Empty before `boot()` succeeds.
95
142
  */
96
143
  readonly services: TServices;
@@ -149,19 +196,28 @@ export type DotAppConfigured<TServices extends ServiceRecord> = {
149
196
  /**
150
197
  * Builder produced by `defineApp(name)`.
151
198
  *
152
- * `.use(pip)` is type-tracking in both directions: the pip's `needs` must
199
+ * `.use(plugin)` is type-tracking in both directions: the plugin's `needs` must
153
200
  * be satisfied by the services accumulated so far, and its `provides`
154
201
  * merge into the accumulated record for the next `.use()`.
155
202
  */
156
203
  export type DotAppBuilder<TAvail extends ServiceRecord> = {
157
204
  /**
158
- * Register a pip. Compile error when the pip's needs are unsatisfied
205
+ * Register a plugin. Compile error when the plugin's needs are unsatisfied
159
206
  * or its provides collide with existing wire keys.
160
207
  */
161
208
  use<TNeeds extends ServiceRecord, TProvides extends ServiceRecord>(
162
- pip: Pip<TNeeds, TProvides>,
209
+ plugin: Plugin<TNeeds, TProvides>,
163
210
  ...guard: UseGuard<TAvail, TNeeds, NormalizeProvides<TProvides>>
164
211
  ): DotAppBuilder<TAvail & NormalizeProvides<TProvides>>;
212
+ /**
213
+ * Register a tuple of plugins in declaration order. This is type-equivalent
214
+ * to calling `.use()` for each item and keeps the same ordering/collision
215
+ * guard across the whole tuple.
216
+ */
217
+ useAll<const TPlugins extends readonly AnyPlugin[]>(
218
+ plugins: TPlugins,
219
+ ...guard: UseAllGuard<TAvail, TPlugins>
220
+ ): DotAppBuilder<UseAllAvail<TAvail, TPlugins>>;
165
221
  /** Run the configure phase synchronously. Throws on configure failure. */
166
222
  configure(): DotAppConfigured<TAvail>;
167
223
  /** Run configure + boot. Throws on configure or boot failure. */
@@ -173,7 +229,7 @@ export type DotAppBuilder<TAvail extends ServiceRecord> = {
173
229
  type BuilderState = {
174
230
  appName: string;
175
231
  appVersion?: string;
176
- pips: AnyPip[];
232
+ plugins: AnyPlugin[];
177
233
  config?: Readonly<Record<string, unknown>>;
178
234
  observers?: readonly DotLifecycleObserver[];
179
235
  hookTimeoutMs?: number;
@@ -184,8 +240,8 @@ type BuilderState = {
184
240
  *
185
241
  * @example
186
242
  * const app = await defineApp('my-app')
187
- * .use(dbPip)
188
- * .use(billingPip) // billing's needs must be satisfied by now
243
+ * .use(dbPlugin)
244
+ * .use(billingPlugin) // billing's needs must be satisfied by now
189
245
  * .boot();
190
246
  *
191
247
  * await app.start();
@@ -208,7 +264,7 @@ export function defineApp(
208
264
  /**
209
265
  * Watchdog budget (ms) for each async hook invocation (`boot`, `start`,
210
266
  * `stop`, `dispose` — `configure` is sync). A hook exceeding the budget
211
- * fails with `DOT_LIFECYCLE_E015` naming the pip and hook, and the
267
+ * fails with `DOT_LIFECYCLE_E015` naming the plugin and hook, and the
212
268
  * kernel applies its normal failure rules (boot rollback, teardown
213
269
  * aggregation). The hook's promise itself cannot be cancelled — the
214
270
  * watchdog makes the hang *visible*, it does not kill it. Default:
@@ -220,7 +276,7 @@ export function defineApp(
220
276
  const state: BuilderState = {
221
277
  appName: name,
222
278
  appVersion: options.version,
223
- pips: [],
279
+ plugins: [],
224
280
  config: options.config,
225
281
  observers: options.observers,
226
282
  hookTimeoutMs: options.hookTimeoutMs,
@@ -232,7 +288,7 @@ function buildImpl(state: BuilderState): DotAppImpl {
232
288
  return new DotAppImpl({
233
289
  appName: state.appName,
234
290
  appVersion: state.appVersion,
235
- pips: state.pips,
291
+ plugins: state.plugins,
236
292
  config: state.config,
237
293
  observers: state.observers,
238
294
  hookTimeoutMs: state.hookTimeoutMs,
@@ -244,10 +300,17 @@ function makeBuilder<TAvail extends ServiceRecord>(state: BuilderState): DotAppB
244
300
  // at the type level); the single cast below is the same kernel boundary
245
301
  // v1 crossed in its wrapApp helper.
246
302
  const impl = {
247
- use(pip: AnyPip, ..._guard: readonly unknown[]): DotAppBuilder<ServiceRecord> {
303
+ use(plugin: AnyPlugin, ..._guard: readonly unknown[]): DotAppBuilder<ServiceRecord> {
304
+ const nextState: BuilderState = {
305
+ ...state,
306
+ plugins: [...state.plugins, plugin],
307
+ };
308
+ return makeBuilder<ServiceRecord>(nextState);
309
+ },
310
+ useAll(plugins: readonly AnyPlugin[], ..._guard: readonly unknown[]): DotAppBuilder<ServiceRecord> {
248
311
  const nextState: BuilderState = {
249
312
  ...state,
250
- pips: [...state.pips, pip],
313
+ plugins: [...state.plugins, ...plugins],
251
314
  };
252
315
  return makeBuilder<ServiceRecord>(nextState);
253
316
  },