@arki/dot 0.1.4 → 0.2.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.
- package/README.md +57 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/render-graph.d.ts +36 -0
- package/dist/cli/render-graph.d.ts.map +1 -0
- package/dist/cli/render-graph.js +70 -0
- package/dist/cli/render-graph.js.map +1 -0
- package/dist/define-app.d.ts +10 -0
- package/dist/define-app.d.ts.map +1 -1
- package/dist/define-app.js +2 -0
- package/dist/define-app.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel/app-instance.d.ts +2 -0
- package/dist/kernel/app-instance.d.ts.map +1 -1
- package/dist/kernel/app-instance.js +67 -17
- package/dist/kernel/app-instance.js.map +1 -1
- package/dist/lifecycle.d.ts +2 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +2 -0
- package/dist/lifecycle.js.map +1 -1
- package/dist/signals.d.ts +65 -0
- package/dist/signals.d.ts.map +1 -0
- package/dist/signals.js +97 -0
- package/dist/signals.js.map +1 -0
- package/dist/test-harness.d.ts +75 -1
- package/dist/test-harness.d.ts.map +1 -1
- package/dist/test-harness.js +71 -0
- package/dist/test-harness.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +36 -2
- package/src/cli/render-graph.ts +88 -0
- package/src/define-app.ts +13 -0
- package/src/index.ts +5 -2
- package/src/kernel/app-instance.ts +113 -51
- package/src/lifecycle.ts +2 -0
- package/src/signals.ts +134 -0
- package/src/test-harness.ts +140 -1
package/dist/test-harness.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* });
|
|
26
26
|
*/
|
|
27
27
|
import type { DotApp, DotAppBuilder } from './define-app.js';
|
|
28
|
-
import type { AnyPip, EmptyShape, ServiceRecord } from './pip-contract.js';
|
|
28
|
+
import type { AnyPip, EmptyShape, Pip, ServiceRecord, Token } from './pip-contract.js';
|
|
29
29
|
export type TestAppOptions = {
|
|
30
30
|
/** App name used in the manifest. Defaults to `'test-app'`. */
|
|
31
31
|
name?: string;
|
|
@@ -42,4 +42,78 @@ export declare function testApp<TServices extends ServiceRecord = EmptyShape>(pi
|
|
|
42
42
|
* calling `app.dispose()` when finished.
|
|
43
43
|
*/
|
|
44
44
|
export declare function bootTestApp<TServices extends ServiceRecord = EmptyShape>(pips?: readonly AnyPip[], options?: TestAppOptions): Promise<DotApp<TServices>>;
|
|
45
|
+
/**
|
|
46
|
+
* Rest-tuple guard for {@link TestPipBuilder.boot} — the same trick the app
|
|
47
|
+
* builder's `UseGuard` uses. While any need is still unprovided, `boot()`
|
|
48
|
+
* demands an impossible second argument, so the call site fails with
|
|
49
|
+
* "Expected 2 arguments, but got 1" and the error payload names the
|
|
50
|
+
* missing wire keys and their types.
|
|
51
|
+
*/
|
|
52
|
+
type TestPipBootGuard<TRemaining extends ServiceRecord> = keyof TRemaining extends never ? readonly [] : readonly [
|
|
53
|
+
needs: {
|
|
54
|
+
readonly 'DOT-TEST: pip needs still unprovided — call .provide() for each': {
|
|
55
|
+
readonly [K in keyof TRemaining]: TRemaining[K];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* Typed unit-test builder for a single pip — see {@link testPip}.
|
|
61
|
+
*
|
|
62
|
+
* `TRemaining` tracks the wire keys not yet covered by `.provide()`;
|
|
63
|
+
* `TServices` accumulates what the booted app will expose (the fakes plus
|
|
64
|
+
* the pip's own provides).
|
|
65
|
+
*/
|
|
66
|
+
export type TestPipBuilder<TRemaining extends ServiceRecord, TServices extends ServiceRecord> = {
|
|
67
|
+
/**
|
|
68
|
+
* Satisfy one need directly with a fake. Token form — the token names
|
|
69
|
+
* the wire key and carries the type:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* testPip(reports).provide(Db, fakeDb)
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
provide<K extends keyof TRemaining & string, T extends TRemaining[K]>(token: Token<T, K>, value: T): TestPipBuilder<Omit<TRemaining, K>, TServices & Readonly<Record<K, T>>>;
|
|
76
|
+
/**
|
|
77
|
+
* Satisfy one need directly with a fake. Key form — for anonymous
|
|
78
|
+
* `service<T>()` needs, where the wire key is the property name:
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* testPip(billing).provide('db', fakeDb)
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
provide<K extends keyof TRemaining & string>(key: K, value: TRemaining[K]): TestPipBuilder<Omit<TRemaining, K>, TServices & Readonly<Record<K, TRemaining[K]>>>;
|
|
85
|
+
/** Boot the pip against the provided fakes. Compile error until every need is provided. */
|
|
86
|
+
boot(...guard: TestPipBootGuard<TRemaining>): Promise<DotApp<TServices>>;
|
|
87
|
+
/** Boot + start. Same guard as {@link TestPipBuilder.boot}. */
|
|
88
|
+
start(...guard: TestPipBootGuard<TRemaining>): Promise<DotApp<TServices>>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Unit-test a single pip with **typed overrides** — the compile-checked
|
|
92
|
+
* counterpart to {@link testApp}'s erased arrays.
|
|
93
|
+
*
|
|
94
|
+
* Each `.provide()` satisfies one of the pip's needs directly (no real
|
|
95
|
+
* provider pip, no dependency chain) and removes it from the builder's
|
|
96
|
+
* remaining-needs type. `boot()` only compiles once every need is covered,
|
|
97
|
+
* and a fake of the wrong shape fails at the `.provide()` call site:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* import { testPip } from '@arki/dot/test-harness';
|
|
101
|
+
*
|
|
102
|
+
* const app = await testPip(catalog)
|
|
103
|
+
* .provide(Db, fakeDb) // token need
|
|
104
|
+
* .provide('cache', fakeKv) // anonymous need — wire key is the alias
|
|
105
|
+
* .boot();
|
|
106
|
+
*
|
|
107
|
+
* expect(app.services.catalog.list()).toEqual([]);
|
|
108
|
+
* await app.dispose();
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* `service.lazy<T>()` needs accept either a plain `T` fake (the kernel
|
|
112
|
+
* lifts it) or a `Lazy<T>` handle (`lazyOf(value)` is handy here).
|
|
113
|
+
* Lifecycle semantics are the real kernel's — the fakes are published by a
|
|
114
|
+
* synthetic first pip, so reverse-order teardown and lazy auto-dispose
|
|
115
|
+
* behave exactly as in production.
|
|
116
|
+
*/
|
|
117
|
+
export declare function testPip<TNeeds extends ServiceRecord, TProvides extends ServiceRecord>(pip: Pip<TNeeds, TProvides>, options?: TestAppOptions): TestPipBuilder<TNeeds, TProvides>;
|
|
118
|
+
export {};
|
|
45
119
|
//# sourceMappingURL=test-harness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-harness.d.ts","sourceRoot":"","sources":["../src/test-harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"test-harness.d.ts","sourceRoot":"","sources":["../src/test-harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGvF,MAAM,MAAM,cAAc,GAAG;IAC3B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C,CAAC;AAWF;;;GAGG;AACH,wBAAgB,OAAO,CAAC,SAAS,SAAS,aAAa,GAAG,UAAU,EAClE,IAAI,GAAE,SAAS,MAAM,EAAO,EAC5B,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAAC,SAAS,CAAC,CAM1B;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,SAAS,SAAS,aAAa,GAAG,UAAU,EAC5E,IAAI,GAAE,SAAS,MAAM,EAAO,EAC5B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAE5B;AAED;;;;;;GAMG;AACH,KAAK,gBAAgB,CAAC,UAAU,SAAS,aAAa,IAAI,MAAM,UAAU,SAAS,KAAK,GACpF,SAAS,EAAE,GACX,SAAS;IACP,KAAK,EAAE;QACL,QAAQ,CAAC,iEAAiE,EAAE;YAC1E,QAAQ,EAAE,CAAC,IAAI,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC;SAChD,CAAC;KACH;CACF,CAAC;AAEN;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,CAAC,UAAU,SAAS,aAAa,EAAE,SAAS,SAAS,aAAa,IAAI;IAC9F;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EAClE,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAClB,KAAK,EAAE,CAAC,GACP,cAAc,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,SAAS,MAAM,UAAU,GAAG,MAAM,EACzC,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GACnB,cAAc,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvF,2FAA2F;IAC3F,IAAI,CAAC,GAAG,KAAK,EAAE,gBAAgB,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACzE,+DAA+D;IAC/D,KAAK,CAAC,GAAG,KAAK,EAAE,gBAAgB,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;CAC3E,CAAC;AASF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,OAAO,CAAC,MAAM,SAAS,aAAa,EAAE,SAAS,SAAS,aAAa,EACnF,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EAC3B,OAAO,GAAE,cAAmB,GAC3B,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,CAInC"}
|
package/dist/test-harness.js
CHANGED
|
@@ -43,4 +43,75 @@ export function testApp(pips = [], options = {}) {
|
|
|
43
43
|
export async function bootTestApp(pips = [], options = {}) {
|
|
44
44
|
return testApp(pips, options).boot();
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Unit-test a single pip with **typed overrides** — the compile-checked
|
|
48
|
+
* counterpart to {@link testApp}'s erased arrays.
|
|
49
|
+
*
|
|
50
|
+
* Each `.provide()` satisfies one of the pip's needs directly (no real
|
|
51
|
+
* provider pip, no dependency chain) and removes it from the builder's
|
|
52
|
+
* remaining-needs type. `boot()` only compiles once every need is covered,
|
|
53
|
+
* and a fake of the wrong shape fails at the `.provide()` call site:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { testPip } from '@arki/dot/test-harness';
|
|
57
|
+
*
|
|
58
|
+
* const app = await testPip(catalog)
|
|
59
|
+
* .provide(Db, fakeDb) // token need
|
|
60
|
+
* .provide('cache', fakeKv) // anonymous need — wire key is the alias
|
|
61
|
+
* .boot();
|
|
62
|
+
*
|
|
63
|
+
* expect(app.services.catalog.list()).toEqual([]);
|
|
64
|
+
* await app.dispose();
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* `service.lazy<T>()` needs accept either a plain `T` fake (the kernel
|
|
68
|
+
* lifts it) or a `Lazy<T>` handle (`lazyOf(value)` is handy here).
|
|
69
|
+
* Lifecycle semantics are the real kernel's — the fakes are published by a
|
|
70
|
+
* synthetic first pip, so reverse-order teardown and lazy auto-dispose
|
|
71
|
+
* behave exactly as in production.
|
|
72
|
+
*/
|
|
73
|
+
export function testPip(pip, options = {}) {
|
|
74
|
+
// Erasure seam — same boundary as `makeBuilder` in define-app.ts: the
|
|
75
|
+
// runtime impl is untyped, the type parameters do all the guarding.
|
|
76
|
+
return makeTestPipBuilder({ pip: pip, fakes: {}, options });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Erased implementation behind {@link testPip}. Mirrors `makeBuilder` in
|
|
80
|
+
* `define-app.ts`: the runtime is untyped, the single cast at the return
|
|
81
|
+
* is the seam, and the type parameters do all the guarding.
|
|
82
|
+
*/
|
|
83
|
+
function makeTestPipBuilder(state) {
|
|
84
|
+
const bootApp = async () => {
|
|
85
|
+
const pips = [];
|
|
86
|
+
if (Object.keys(state.fakes).length > 0) {
|
|
87
|
+
const fakes = { ...state.fakes };
|
|
88
|
+
pips.push({
|
|
89
|
+
name: `test:fakes(${state.pip.name})`,
|
|
90
|
+
needs: {},
|
|
91
|
+
renames: {},
|
|
92
|
+
hooks: { boot: () => fakes },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
pips.push(state.pip);
|
|
96
|
+
return testApp(pips, {
|
|
97
|
+
name: state.options.name ?? `test:${state.pip.name}`,
|
|
98
|
+
...(state.options.config === undefined ? {} : { config: state.options.config }),
|
|
99
|
+
}).boot();
|
|
100
|
+
};
|
|
101
|
+
const impl = {
|
|
102
|
+
provide(tokenOrKey, value) {
|
|
103
|
+
const key = typeof tokenOrKey === 'string' ? tokenOrKey : tokenOrKey.key;
|
|
104
|
+
return makeTestPipBuilder({ ...state, fakes: { ...state.fakes, [key]: value } });
|
|
105
|
+
},
|
|
106
|
+
async boot(..._guard) {
|
|
107
|
+
return bootApp();
|
|
108
|
+
},
|
|
109
|
+
async start(..._guard) {
|
|
110
|
+
const app = await bootApp();
|
|
111
|
+
await app.start();
|
|
112
|
+
return app;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
return impl;
|
|
116
|
+
}
|
|
46
117
|
//# sourceMappingURL=test-harness.js.map
|
package/dist/test-harness.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-harness.js","sourceRoot":"","sources":["../src/test-harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAkB5C;;;GAGG;AACH,MAAM,UAAU,OAAO,CACrB,OAA0B,EAAE,EAC5B,UAA0B,EAAE;IAE5B,IAAI,OAAO,GAAY,SAAS,CAAC,OAAO,CAAC,IAAI,IAAI,UAAU,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzF,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,OAAO,GAAI,OAAwB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,OAAmC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAA0B,EAAE,EAC5B,UAA0B,EAAE;IAE5B,OAAO,OAAO,CAAY,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAClD,CAAC"}
|
|
1
|
+
{"version":3,"file":"test-harness.js","sourceRoot":"","sources":["../src/test-harness.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arki/dot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "TypeScript-first application composition framework for the ARKI package family — lifecycle-aware pips, deterministic boot order, manifest/diagnostics envelopes, and an agent-native CLI. A DOT app is the sum of its pips.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/cli/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { runNew } from './new.js';
|
|
|
33
33
|
import { probeObservability } from './observability-probe.js';
|
|
34
34
|
import { renderDoctor } from './render-doctor.js';
|
|
35
35
|
import { renderExplain } from './render-explain.js';
|
|
36
|
+
import { renderGraph } from './render-graph.js';
|
|
36
37
|
|
|
37
38
|
const debugCli = createDebugLogger('arki:dot:cli');
|
|
38
39
|
|
|
@@ -57,6 +58,10 @@ Common options:
|
|
|
57
58
|
--app <path> Path to the app entry file (default: discovers
|
|
58
59
|
./dot.config.ts, ./src/app.ts, or ./app.ts)
|
|
59
60
|
--cwd <dir> Working directory (default: current)
|
|
61
|
+
--graph Emit the pip graph as Mermaid flowchart source
|
|
62
|
+
instead of the standard output. explain shows
|
|
63
|
+
declaration (= boot) order; doctor shows the
|
|
64
|
+
wiring observed during boot. Composes with --json.
|
|
60
65
|
|
|
61
66
|
\`doctor\` options:
|
|
62
67
|
--observability Also probe whether an OpenTelemetry SDK is
|
|
@@ -89,6 +94,8 @@ export type CliArgs = {
|
|
|
89
94
|
force?: boolean;
|
|
90
95
|
/** `--observability` (only honored by `doctor`). */
|
|
91
96
|
observability?: boolean;
|
|
97
|
+
/** `--graph` (honored by `explain` and `doctor`). */
|
|
98
|
+
graph?: boolean;
|
|
92
99
|
};
|
|
93
100
|
|
|
94
101
|
/**
|
|
@@ -152,6 +159,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
152
159
|
'dry-run': { type: 'boolean', default: false },
|
|
153
160
|
force: { type: 'boolean', default: false },
|
|
154
161
|
observability: { type: 'boolean', default: false },
|
|
162
|
+
graph: { type: 'boolean', default: false },
|
|
155
163
|
},
|
|
156
164
|
});
|
|
157
165
|
} catch (err) {
|
|
@@ -174,6 +182,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
174
182
|
'dry-run'?: boolean;
|
|
175
183
|
force?: boolean;
|
|
176
184
|
observability?: boolean;
|
|
185
|
+
graph?: boolean;
|
|
177
186
|
};
|
|
178
187
|
|
|
179
188
|
if (values.help) command = 'help';
|
|
@@ -205,6 +214,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
205
214
|
dryRun: values['dry-run'] ?? false,
|
|
206
215
|
force: values.force ?? false,
|
|
207
216
|
observability: values.observability ?? false,
|
|
217
|
+
graph: values.graph ?? false,
|
|
208
218
|
};
|
|
209
219
|
}
|
|
210
220
|
|
|
@@ -220,7 +230,7 @@ async function loadApp(args: CliArgs): Promise<DiscoveredApp> {
|
|
|
220
230
|
*/
|
|
221
231
|
export async function runExplain(
|
|
222
232
|
discovered: DiscoveredApp,
|
|
223
|
-
opts: { json: boolean; out?: (line: string) => void; now?: () => Date },
|
|
233
|
+
opts: { json: boolean; graph?: boolean; out?: (line: string) => void; now?: () => Date },
|
|
224
234
|
): Promise<DotCliEnvelope<unknown>> {
|
|
225
235
|
let configured: DotApp<Record<string, unknown>> | DotAppConfigured<Record<string, unknown>>;
|
|
226
236
|
try {
|
|
@@ -236,6 +246,12 @@ export async function runExplain(
|
|
|
236
246
|
throw wrapLifecycleError(err, 'configure');
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
if (opts.graph === true) {
|
|
250
|
+
return renderGraph(
|
|
251
|
+
{ manifest: configured.manifest, command: 'explain' },
|
|
252
|
+
{ json: opts.json, out: opts.out, now: opts.now },
|
|
253
|
+
);
|
|
254
|
+
}
|
|
239
255
|
return renderExplain({ manifest: configured.manifest }, { json: opts.json, out: opts.out, now: opts.now });
|
|
240
256
|
}
|
|
241
257
|
|
|
@@ -249,6 +265,8 @@ type DoctorRunOptions = {
|
|
|
249
265
|
* present. Default `false`.
|
|
250
266
|
*/
|
|
251
267
|
observability?: boolean;
|
|
268
|
+
/** When `true`, emit the pip graph (Mermaid) instead of diagnostics. */
|
|
269
|
+
graph?: boolean;
|
|
252
270
|
};
|
|
253
271
|
|
|
254
272
|
/**
|
|
@@ -268,6 +286,12 @@ export async function runDoctor(
|
|
|
268
286
|
// Already-booted app: just read diagnostics, don't touch lifecycle.
|
|
269
287
|
if (!guards.isDotAppBuilder(discovered) && !guards.isDotAppConfigured(discovered)) {
|
|
270
288
|
const app = discovered as DotApp<Record<string, unknown>>;
|
|
289
|
+
if (opts.graph === true) {
|
|
290
|
+
return renderGraph(
|
|
291
|
+
{ manifest: app.manifest, command: 'doctor' },
|
|
292
|
+
{ json: opts.json, out: opts.out, now: opts.now },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
271
295
|
const diagnostics = applyObservabilityProbe(app.diagnostics, opts.observability ?? false);
|
|
272
296
|
return renderDoctor({ diagnostics }, { json: opts.json, out: opts.out, now: opts.now });
|
|
273
297
|
}
|
|
@@ -297,6 +321,16 @@ export async function runDoctor(
|
|
|
297
321
|
}
|
|
298
322
|
|
|
299
323
|
try {
|
|
324
|
+
if (opts.graph === true) {
|
|
325
|
+
// Post-boot manifest carries the observed wiring edges; after a boot
|
|
326
|
+
// failure it carries the edges recorded up to the failing pip.
|
|
327
|
+
const manifest = bootedApp ? bootedApp.manifest : configured.manifest;
|
|
328
|
+
const graphEnvelope = renderGraph(
|
|
329
|
+
{ manifest, command: 'doctor' },
|
|
330
|
+
{ json: opts.json, out: opts.out, now: opts.now },
|
|
331
|
+
);
|
|
332
|
+
return bootThrew ? { ...graphEnvelope, status: 'failure' } : graphEnvelope;
|
|
333
|
+
}
|
|
300
334
|
const rawDiagnostics = bootedApp ? bootedApp.diagnostics : configured.diagnostics;
|
|
301
335
|
const diagnostics = applyObservabilityProbe(rawDiagnostics, opts.observability ?? false);
|
|
302
336
|
const envelope = renderDoctor({ diagnostics }, { json: opts.json, out: opts.out, now: opts.now });
|
|
@@ -450,7 +484,7 @@ export async function main(options: MainOptions): Promise<number> {
|
|
|
450
484
|
|
|
451
485
|
try {
|
|
452
486
|
const discovered = await loadApp(args);
|
|
453
|
-
const opts = { json: args.json, out: stdout, now: nowFactory };
|
|
487
|
+
const opts = { json: args.json, graph: args.graph, out: stdout, now: nowFactory };
|
|
454
488
|
let envelope: DotCliEnvelope<unknown>;
|
|
455
489
|
|
|
456
490
|
if (args.command === 'explain') {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer for `dot explain --graph` / `dot doctor --graph`.
|
|
3
|
+
*
|
|
4
|
+
* Emits the app's pip graph as [Mermaid](https://mermaid.js.org) `flowchart`
|
|
5
|
+
* source — paste-able into GitHub markdown, docs, and mermaid.live.
|
|
6
|
+
*
|
|
7
|
+
* The nodes are the pips in declaration order (which IS boot order in v2 —
|
|
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
|
|
10
|
+
* was satisfied during boot. `explain` never boots, so its graph shows
|
|
11
|
+
* declaration order with whatever edges configure-time metadata declared;
|
|
12
|
+
* `doctor` boots, so its graph shows the real wiring.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DotAppManifest } from '../manifest.js';
|
|
16
|
+
import type { DotCliEnvelope, RenderOptions } from './render-explain.js';
|
|
17
|
+
|
|
18
|
+
/** Envelope payload for graph output. */
|
|
19
|
+
export type GraphData = {
|
|
20
|
+
format: 'mermaid';
|
|
21
|
+
/** Mermaid `flowchart` source. */
|
|
22
|
+
source: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Escape a pip name for use inside a quoted Mermaid node label. */
|
|
26
|
+
function escapeLabel(name: string): string {
|
|
27
|
+
return name.replaceAll('"', '#quot;');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build Mermaid `flowchart` source from a manifest. Deterministic: node
|
|
32
|
+
* ids follow declaration order, edges follow manifest order.
|
|
33
|
+
*/
|
|
34
|
+
export function buildMermaidGraph(manifest: DotAppManifest): string {
|
|
35
|
+
const lines: string[] = ['flowchart TD'];
|
|
36
|
+
const idByPip = new Map<string, string>();
|
|
37
|
+
|
|
38
|
+
for (const [index, pip] of manifest.pips.entries()) {
|
|
39
|
+
const id = `p${index.toString()}`;
|
|
40
|
+
idByPip.set(pip.name, id);
|
|
41
|
+
const order = (index + 1).toString();
|
|
42
|
+
const version = pip.version === undefined ? '' : `@${pip.version}`;
|
|
43
|
+
lines.push(` ${id}["${order} · ${escapeLabel(pip.name)}${escapeLabel(version)}"]`);
|
|
44
|
+
}
|
|
45
|
+
|
|
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
|
|
50
|
+
// than emit invalid Mermaid.
|
|
51
|
+
if (from === undefined || to === undefined) continue;
|
|
52
|
+
lines.push(` ${from} -->|${edge.kind}| ${to}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `${lines.join('\n')}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render the graph output. Plain mode prints raw Mermaid source (pipe it
|
|
60
|
+
* straight into a markdown code fence); `--json` wraps it in the standard
|
|
61
|
+
* CLI envelope under `data.source`.
|
|
62
|
+
*/
|
|
63
|
+
export function renderGraph(
|
|
64
|
+
source: { manifest: DotAppManifest; command: 'explain' | 'doctor' },
|
|
65
|
+
opts: RenderOptions,
|
|
66
|
+
): DotCliEnvelope<GraphData> {
|
|
67
|
+
const mermaid = buildMermaidGraph(source.manifest);
|
|
68
|
+
const nowFactory = opts.now ?? (() => new Date());
|
|
69
|
+
const envelope: DotCliEnvelope<GraphData> = {
|
|
70
|
+
status: 'success',
|
|
71
|
+
command: source.command,
|
|
72
|
+
generatedAt: nowFactory().toISOString(),
|
|
73
|
+
data: { format: 'mermaid', source: mermaid },
|
|
74
|
+
errors: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const out =
|
|
78
|
+
opts.out ??
|
|
79
|
+
((line: string) => {
|
|
80
|
+
process.stdout.write(line);
|
|
81
|
+
});
|
|
82
|
+
if (opts.json) {
|
|
83
|
+
out(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
84
|
+
} else {
|
|
85
|
+
out(mermaid);
|
|
86
|
+
}
|
|
87
|
+
return envelope;
|
|
88
|
+
}
|
package/src/define-app.ts
CHANGED
|
@@ -176,6 +176,7 @@ type BuilderState = {
|
|
|
176
176
|
pips: AnyPip[];
|
|
177
177
|
config?: Readonly<Record<string, unknown>>;
|
|
178
178
|
observers?: readonly DotLifecycleObserver[];
|
|
179
|
+
hookTimeoutMs?: number;
|
|
179
180
|
};
|
|
180
181
|
|
|
181
182
|
/**
|
|
@@ -204,6 +205,16 @@ export function defineApp(
|
|
|
204
205
|
* `configured.subscribe(...)` or `app.subscribe(...)`.
|
|
205
206
|
*/
|
|
206
207
|
observers?: readonly DotLifecycleObserver[];
|
|
208
|
+
/**
|
|
209
|
+
* Watchdog budget (ms) for each async hook invocation (`boot`, `start`,
|
|
210
|
+
* `stop`, `dispose` — `configure` is sync). A hook exceeding the budget
|
|
211
|
+
* fails with `DOT_LIFECYCLE_E015` naming the pip and hook, and the
|
|
212
|
+
* kernel applies its normal failure rules (boot rollback, teardown
|
|
213
|
+
* aggregation). The hook's promise itself cannot be cancelled — the
|
|
214
|
+
* watchdog makes the hang *visible*, it does not kill it. Default:
|
|
215
|
+
* no watchdog.
|
|
216
|
+
*/
|
|
217
|
+
hookTimeoutMs?: number;
|
|
207
218
|
} = {},
|
|
208
219
|
): DotAppBuilder<EmptyShape> {
|
|
209
220
|
const state: BuilderState = {
|
|
@@ -212,6 +223,7 @@ export function defineApp(
|
|
|
212
223
|
pips: [],
|
|
213
224
|
config: options.config,
|
|
214
225
|
observers: options.observers,
|
|
226
|
+
hookTimeoutMs: options.hookTimeoutMs,
|
|
215
227
|
};
|
|
216
228
|
return makeBuilder<EmptyShape>(state);
|
|
217
229
|
}
|
|
@@ -223,6 +235,7 @@ function buildImpl(state: BuilderState): DotAppImpl {
|
|
|
223
235
|
pips: state.pips,
|
|
224
236
|
config: state.config,
|
|
225
237
|
observers: state.observers,
|
|
238
|
+
hookTimeoutMs: state.hookTimeoutMs,
|
|
226
239
|
});
|
|
227
240
|
}
|
|
228
241
|
|
package/src/index.ts
CHANGED
|
@@ -80,8 +80,11 @@ export type {
|
|
|
80
80
|
export { renderTimeline } from './timeline.js';
|
|
81
81
|
export type { RenderTimelineOptions } from './timeline.js';
|
|
82
82
|
|
|
83
|
-
export { testApp, bootTestApp } from './test-harness.js';
|
|
84
|
-
export type { TestAppOptions } from './test-harness.js';
|
|
83
|
+
export { testApp, bootTestApp, testPip } from './test-harness.js';
|
|
84
|
+
export type { TestAppOptions, TestPipBuilder } from './test-harness.js';
|
|
85
|
+
|
|
86
|
+
export { hookSignals } from './signals.js';
|
|
87
|
+
export type { HookSignalsOptions, SignalTarget } from './signals.js';
|
|
85
88
|
|
|
86
89
|
// Task 9b: CLI envelope type is exported so adapter packages can produce the
|
|
87
90
|
// same shape from related tooling (release-tooling, pip scaffolds, etc.).
|