@nwire/scan 0.12.1 → 0.13.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/dist/ast-extract.d.ts +71 -0
- package/dist/ast-extract.js +1024 -0
- package/dist/graph.d.ts +56 -0
- package/dist/graph.js +325 -0
- package/dist/manifest.d.ts +67 -0
- package/dist/manifest.js +103 -0
- package/dist/scan.d.ts +46 -108
- package/dist/scan.js +13 -381
- package/dist/telemetry-runs.d.ts +37 -0
- package/dist/telemetry-runs.js +87 -0
- package/dist/topology.d.ts +10 -0
- package/dist/topology.js +10 -0
- package/dist/vite-plugin.d.ts +14 -6
- package/dist/vite-plugin.js +27 -12
- package/package.json +8 -6
package/dist/scan.d.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nwire/scan` —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* workflows, and external calls. It also reads `container.list()` for
|
|
8
|
-
* DI bindings and `runtime.listHooks()` for hooks.
|
|
9
|
-
*
|
|
10
|
-
* Callers are responsible for booting their apps before scanning — the
|
|
11
|
-
* forge plugin populates the dispatcher during `app.start()`.
|
|
2
|
+
* `@nwire/scan` — the static type surface (the `*Entry` shapes) shared by the
|
|
3
|
+
* AST extractor + the build-time manifest, plus re-exports of the manifest API
|
|
4
|
+
* (`buildManifest` / `writeManifest` / `readManifest`, the topology capture, and
|
|
5
|
+
* the node+edge graph). The old runtime boot-and-walk (`buildCache`) is gone —
|
|
6
|
+
* `nwire cache` emits `.nwire/manifest.json` via `buildManifest`.
|
|
12
7
|
*/
|
|
13
8
|
export interface SourceLocationEntry {
|
|
14
9
|
readonly file: string;
|
|
15
10
|
readonly line?: number;
|
|
16
11
|
readonly column?: number;
|
|
17
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A business-rule invariant captured from a `validate()` predicate: the
|
|
15
|
+
* predicate's source text (`rule`) and its English failure message (`message`)
|
|
16
|
+
* when one is statically present.
|
|
17
|
+
*/
|
|
18
|
+
export interface InvariantEntry {
|
|
19
|
+
readonly rule: string;
|
|
20
|
+
readonly message?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A state-machine edge: `from --on--> to` (actor/workflow). `from: "*"` = any state. */
|
|
23
|
+
export interface TransitionEdge {
|
|
24
|
+
readonly from: string;
|
|
25
|
+
readonly on: string;
|
|
26
|
+
readonly to: string;
|
|
27
|
+
}
|
|
18
28
|
export interface ActionEntry {
|
|
19
29
|
readonly name: string;
|
|
20
30
|
readonly app: string;
|
|
@@ -23,6 +33,10 @@ export interface ActionEntry {
|
|
|
23
33
|
readonly inputSchema?: unknown;
|
|
24
34
|
/** Event names this action emits, in declaration order. */
|
|
25
35
|
readonly emits: readonly string[];
|
|
36
|
+
/** External-call names invoked in the handler body (ctx.externalCall). */
|
|
37
|
+
readonly calls?: readonly string[];
|
|
38
|
+
/** Business-rule invariants surfaced from `validate()` predicates in the handler. */
|
|
39
|
+
readonly invariants?: readonly InvariantEntry[];
|
|
26
40
|
/** True when a handler was wired via defineAction({handler}) — false for split definitions. */
|
|
27
41
|
readonly hasInlineHandler: boolean;
|
|
28
42
|
readonly persona?: string;
|
|
@@ -82,6 +96,10 @@ export interface WorkflowEntry {
|
|
|
82
96
|
readonly subscribesTo: readonly string[];
|
|
83
97
|
/** Action names this workflow dispatches inside its body. */
|
|
84
98
|
readonly dispatches: readonly string[];
|
|
99
|
+
/** State→state transitions (saga state machine), `from: "*"` = always-active `when`. */
|
|
100
|
+
readonly transitions?: readonly TransitionEdge[];
|
|
101
|
+
/** External-call names invoked in the body. */
|
|
102
|
+
readonly calls?: readonly string[];
|
|
85
103
|
readonly source?: SourceLocationEntry;
|
|
86
104
|
}
|
|
87
105
|
export interface EventEntry {
|
|
@@ -99,6 +117,10 @@ export interface ActorEntry {
|
|
|
99
117
|
readonly app: string;
|
|
100
118
|
/** State names declared in the actor's `states` map. */
|
|
101
119
|
readonly states: readonly string[];
|
|
120
|
+
/** State→state transitions from the closure's `when` reactions, `from: "*"` = always-active. */
|
|
121
|
+
readonly transitions?: readonly TransitionEdge[];
|
|
122
|
+
/** Business-rule invariants surfaced from `validate()` predicates in the actor's methods. */
|
|
123
|
+
readonly invariants?: readonly InvariantEntry[];
|
|
102
124
|
readonly source?: SourceLocationEntry;
|
|
103
125
|
}
|
|
104
126
|
export interface ProjectionEntry {
|
|
@@ -117,6 +139,11 @@ export interface QueryEntry {
|
|
|
117
139
|
readonly projection?: string;
|
|
118
140
|
readonly source?: SourceLocationEntry;
|
|
119
141
|
}
|
|
142
|
+
/** A config module the app exposes: its file path and top-level field names. */
|
|
143
|
+
export interface ConfigModuleEntry {
|
|
144
|
+
readonly file: string;
|
|
145
|
+
readonly keys: readonly string[];
|
|
146
|
+
}
|
|
120
147
|
export interface AppEntry {
|
|
121
148
|
readonly name: string;
|
|
122
149
|
readonly description?: string;
|
|
@@ -124,6 +151,10 @@ export interface AppEntry {
|
|
|
124
151
|
readonly plugins: readonly string[];
|
|
125
152
|
readonly tenantModel?: string;
|
|
126
153
|
readonly tenantKey?: string;
|
|
154
|
+
/** Environment variable names the app's source reads (sorted, de-duped). */
|
|
155
|
+
readonly env?: readonly string[];
|
|
156
|
+
/** Config modules discovered under the app's `config/` directory. */
|
|
157
|
+
readonly config?: readonly ConfigModuleEntry[];
|
|
127
158
|
}
|
|
128
159
|
export interface SinkEntry {
|
|
129
160
|
readonly name: string;
|
|
@@ -197,101 +228,8 @@ export interface EventGraphEdge {
|
|
|
197
228
|
readonly to: string;
|
|
198
229
|
readonly via: string;
|
|
199
230
|
}
|
|
200
|
-
export
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
readonly actors: readonly ActorEntry[];
|
|
206
|
-
readonly projections: readonly ProjectionEntry[];
|
|
207
|
-
readonly queries: readonly QueryEntry[];
|
|
208
|
-
readonly resolvers: readonly ResolverEntry[];
|
|
209
|
-
readonly routes: readonly RouteEntry[];
|
|
210
|
-
readonly workflows: readonly WorkflowEntry[];
|
|
211
|
-
readonly externalCalls: readonly ExternalCallEntry[];
|
|
212
|
-
readonly inboundWebhooks: readonly InboundWebhookEntry[];
|
|
213
|
-
readonly outboxes: readonly OutboxEntry[];
|
|
214
|
-
readonly inboxes: readonly InboxEntry[];
|
|
215
|
-
readonly crons: readonly CronEntry[];
|
|
216
|
-
readonly commands: readonly CommandEntry[];
|
|
217
|
-
readonly hooks: readonly HookEntry[];
|
|
218
|
-
readonly plugins: readonly PluginEntry[];
|
|
219
|
-
readonly sinks: readonly SinkEntry[];
|
|
220
|
-
readonly bindings: readonly DIBindingEntry[];
|
|
221
|
-
readonly resources: readonly ResourceEntry[];
|
|
222
|
-
readonly errors: readonly ErrorEntry[];
|
|
223
|
-
readonly middleware: readonly MiddlewareEntry[];
|
|
224
|
-
readonly graph: {
|
|
225
|
-
readonly events: readonly EventGraphEdge[];
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
export type ResourceInput = {
|
|
229
|
-
readonly $kind: "resource";
|
|
230
|
-
} | {
|
|
231
|
-
readonly definition: {
|
|
232
|
-
readonly $kind: "resource";
|
|
233
|
-
} & Record<string, any>;
|
|
234
|
-
readonly app?: string;
|
|
235
|
-
readonly module?: string;
|
|
236
|
-
};
|
|
237
|
-
export type ErrorInput = {
|
|
238
|
-
readonly $kind: "error";
|
|
239
|
-
} | {
|
|
240
|
-
readonly definition: {
|
|
241
|
-
readonly $kind: "error";
|
|
242
|
-
} & Record<string, any>;
|
|
243
|
-
readonly app?: string;
|
|
244
|
-
readonly module?: string;
|
|
245
|
-
};
|
|
246
|
-
export type MiddlewareInput = {
|
|
247
|
-
readonly name: string;
|
|
248
|
-
} | {
|
|
249
|
-
readonly definition: {
|
|
250
|
-
readonly name: string;
|
|
251
|
-
};
|
|
252
|
-
readonly where?: string;
|
|
253
|
-
};
|
|
254
|
-
export interface BuildCacheOptions {
|
|
255
|
-
readonly mounts?: readonly ResolverMount[];
|
|
256
|
-
readonly interfaces?: readonly RouteSource[];
|
|
257
|
-
readonly resources?: readonly ResourceInput[];
|
|
258
|
-
readonly errors?: readonly ErrorInput[];
|
|
259
|
-
readonly middleware?: readonly MiddlewareInput[];
|
|
260
|
-
}
|
|
261
|
-
interface ScannedApp {
|
|
262
|
-
readonly appName: string;
|
|
263
|
-
readonly description?: string;
|
|
264
|
-
dispatcher?: () => any;
|
|
265
|
-
/** Plugin definitions installed via createApp({plugins}) or .with(). */
|
|
266
|
-
readonly plugins?: readonly {
|
|
267
|
-
name: string;
|
|
268
|
-
$source?: SourceLocationEntry;
|
|
269
|
-
}[];
|
|
270
|
-
container: {
|
|
271
|
-
resolve?: <T = any>(name: string) => T;
|
|
272
|
-
list?(): readonly {
|
|
273
|
-
name: string;
|
|
274
|
-
kind: string;
|
|
275
|
-
source?: SourceLocationEntry;
|
|
276
|
-
}[];
|
|
277
|
-
};
|
|
278
|
-
runtime?: {
|
|
279
|
-
listHooks?(): readonly {
|
|
280
|
-
id: string;
|
|
281
|
-
name: string;
|
|
282
|
-
chain: number;
|
|
283
|
-
listeners: number;
|
|
284
|
-
source?: SourceLocationEntry;
|
|
285
|
-
}[];
|
|
286
|
-
listSinkStages?(): readonly {
|
|
287
|
-
name: string;
|
|
288
|
-
kind?: string;
|
|
289
|
-
position: "early" | "middle" | "terminal";
|
|
290
|
-
direction?: "outbound";
|
|
291
|
-
}[];
|
|
292
|
-
hooks?: Record<string, any>;
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
export declare function buildCache(apps: readonly ScannedApp[], options?: BuildCacheOptions): Cache;
|
|
296
|
-
export declare function writeCache(cache: Cache, dir: string): Promise<void>;
|
|
297
|
-
export {};
|
|
231
|
+
export { buildManifest, writeManifest, readManifest, collectSourceFiles, MANIFEST_VERSION, type Manifest, } from "./manifest.js";
|
|
232
|
+
export { extractFromFiles, collectConfigModules, type AstExtract, type SchemaEntry, type ConfigModuleEntry as AstConfigModuleEntry, } from "./ast-extract.js";
|
|
233
|
+
export { captureTopology, type Topology, type CapabilityInfo, type StageInfo, type PluginInfo, type BindingInfo, type HandlerInfo, type HookInfo, type ContributionInfo, type TriggerInfo, } from "./topology.js";
|
|
234
|
+
export { buildGraph, type ManifestModel, type GraphNode, type GraphEdge, type GraphIntent, type EdgeType, } from "./graph.js";
|
|
235
|
+
export { listTelemetryRuns, readTelemetryRun, type TelemetryRunMeta } from "./telemetry-runs.js";
|
package/dist/scan.js
CHANGED
|
@@ -1,383 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nwire/scan` —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* workflows, and external calls. It also reads `container.list()` for
|
|
8
|
-
* DI bindings and `runtime.listHooks()` for hooks.
|
|
9
|
-
*
|
|
10
|
-
* Callers are responsible for booting their apps before scanning — the
|
|
11
|
-
* forge plugin populates the dispatcher during `app.start()`.
|
|
2
|
+
* `@nwire/scan` — the static type surface (the `*Entry` shapes) shared by the
|
|
3
|
+
* AST extractor + the build-time manifest, plus re-exports of the manifest API
|
|
4
|
+
* (`buildManifest` / `writeManifest` / `readManifest`, the topology capture, and
|
|
5
|
+
* the node+edge graph). The old runtime boot-and-walk (`buildCache`) is gone —
|
|
6
|
+
* `nwire cache` emits `.nwire/manifest.json` via `buildManifest`.
|
|
12
7
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const d = app.dispatcher();
|
|
22
|
-
if (d)
|
|
23
|
-
return d;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
/* fall through */
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (typeof app.container?.resolve === "function") {
|
|
30
|
-
try {
|
|
31
|
-
const d = app.container.resolve("forge.dispatcher");
|
|
32
|
-
if (d)
|
|
33
|
-
return d;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
/* not bound — plain app, no forge */
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
function sourceOf(value) {
|
|
42
|
-
return value?.$source;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Coerce a Zod schema into a JSON-Schema-shaped `{ type, properties,
|
|
46
|
-
* required }` value Studio's form renderer + SchemaTree understand. The
|
|
47
|
-
* scanner doesn't carry a Zod dep, so we walk the runtime shape Zod 4
|
|
48
|
-
* publishes (`def.type`, `def.shape`, `def.innerType`, etc.) and project
|
|
49
|
-
* the bits Studio needs.
|
|
50
|
-
*
|
|
51
|
-
* Falls back to the raw value when the shape is unrecognisable so the
|
|
52
|
-
* SchemaTree's "raw JSON" view still shows something.
|
|
53
|
-
*/
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
-
function zodToJsonSchema(schema) {
|
|
56
|
-
if (!schema || typeof schema !== "object")
|
|
57
|
-
return schema;
|
|
58
|
-
const s = schema;
|
|
59
|
-
const def = (s.def ?? s._def ?? {});
|
|
60
|
-
const type = String(def.type ?? "");
|
|
61
|
-
switch (type) {
|
|
62
|
-
case "object": {
|
|
63
|
-
const shape = (def.shape ?? {});
|
|
64
|
-
const properties = {};
|
|
65
|
-
const required = [];
|
|
66
|
-
for (const [key, raw] of Object.entries(shape)) {
|
|
67
|
-
const field = raw;
|
|
68
|
-
const inner = zodToJsonSchema(field);
|
|
69
|
-
properties[key] = inner;
|
|
70
|
-
// A field is required when its def isn't `optional`/`nullable`.
|
|
71
|
-
const innerType = (field.def ?? {}).type;
|
|
72
|
-
if (innerType !== "optional" && innerType !== "nullable") {
|
|
73
|
-
required.push(key);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return { type: "object", properties, required };
|
|
77
|
-
}
|
|
78
|
-
case "string":
|
|
79
|
-
return { type: "string" };
|
|
80
|
-
case "number":
|
|
81
|
-
return { type: "number" };
|
|
82
|
-
case "boolean":
|
|
83
|
-
return { type: "boolean" };
|
|
84
|
-
case "literal":
|
|
85
|
-
return { type: typeof def.value, enum: [def.value] };
|
|
86
|
-
case "enum": {
|
|
87
|
-
const opts = (def.entries ?? def.values ?? []);
|
|
88
|
-
return { type: "string", enum: opts };
|
|
89
|
-
}
|
|
90
|
-
case "array":
|
|
91
|
-
return { type: "array", items: zodToJsonSchema(def.element) };
|
|
92
|
-
case "optional":
|
|
93
|
-
case "nullable":
|
|
94
|
-
case "default":
|
|
95
|
-
return zodToJsonSchema(def.innerType);
|
|
96
|
-
case "union": {
|
|
97
|
-
const opts = (def.options ?? []);
|
|
98
|
-
return { anyOf: opts.map((o) => zodToJsonSchema(o)) };
|
|
99
|
-
}
|
|
100
|
-
case "record":
|
|
101
|
-
return { type: "object", additionalProperties: zodToJsonSchema(def.valueType) };
|
|
102
|
-
default:
|
|
103
|
-
// Unrecognised — return the raw shape so the SchemaTree at least
|
|
104
|
-
// shows the JSON, even if the form renderer can't drive it.
|
|
105
|
-
return schema;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function safeMap(map) {
|
|
109
|
-
return map ?? new Map();
|
|
110
|
-
}
|
|
111
|
-
export function buildCache(apps, options = {}) {
|
|
112
|
-
const out = {
|
|
113
|
-
generatedAt: new Date().toISOString(),
|
|
114
|
-
apps: [],
|
|
115
|
-
actions: [],
|
|
116
|
-
events: [],
|
|
117
|
-
actors: [],
|
|
118
|
-
projections: [],
|
|
119
|
-
queries: [],
|
|
120
|
-
resolvers: [],
|
|
121
|
-
routes: [],
|
|
122
|
-
workflows: [],
|
|
123
|
-
externalCalls: [],
|
|
124
|
-
inboundWebhooks: [],
|
|
125
|
-
outboxes: [],
|
|
126
|
-
inboxes: [],
|
|
127
|
-
crons: [],
|
|
128
|
-
commands: [],
|
|
129
|
-
hooks: [],
|
|
130
|
-
plugins: [],
|
|
131
|
-
sinks: [],
|
|
132
|
-
bindings: [],
|
|
133
|
-
resources: [],
|
|
134
|
-
errors: [],
|
|
135
|
-
middleware: [],
|
|
136
|
-
graph: { events: [] },
|
|
137
|
-
};
|
|
138
|
-
const eventNamesSeen = new Set();
|
|
139
|
-
for (const app of apps) {
|
|
140
|
-
const pluginNames = (app.plugins ?? []).map((p) => p.name);
|
|
141
|
-
out.apps.push({
|
|
142
|
-
name: app.appName,
|
|
143
|
-
description: app.description,
|
|
144
|
-
plugins: pluginNames,
|
|
145
|
-
});
|
|
146
|
-
// ── Plugins ─────────────────────────────────────────────────────
|
|
147
|
-
for (const plugin of app.plugins ?? []) {
|
|
148
|
-
out.plugins.push({
|
|
149
|
-
name: plugin.name,
|
|
150
|
-
kind: "plugin",
|
|
151
|
-
app: app.appName,
|
|
152
|
-
source: plugin.$source,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
// ── Sink stages (outbound) ─────────────────────────────────────
|
|
156
|
-
const sinkStages = app.runtime?.listSinkStages?.() ?? [];
|
|
157
|
-
for (const stage of sinkStages) {
|
|
158
|
-
out.sinks.push({
|
|
159
|
-
name: stage.name,
|
|
160
|
-
app: app.appName,
|
|
161
|
-
kind: stage.kind,
|
|
162
|
-
position: stage.position,
|
|
163
|
-
direction: stage.direction ?? "outbound",
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
const dispatcher = resolveDispatcher(app);
|
|
167
|
-
if (!dispatcher) {
|
|
168
|
-
// Plain HTTP App with no forge — the scanner has nothing to
|
|
169
|
-
// collect from the runtime side. Continue to the next App.
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
// ── Actions (via handlers) ──────────────────────────────────────
|
|
173
|
-
const handlers = safeMap(dispatcher.handlers);
|
|
174
|
-
for (const [name, handler] of handlers) {
|
|
175
|
-
const action = handler.action ?? {};
|
|
176
|
-
const emits = (action.emits ?? []);
|
|
177
|
-
out.actions.push({
|
|
178
|
-
name,
|
|
179
|
-
app: app.appName,
|
|
180
|
-
description: typeof action.description === "string" ? action.description : undefined,
|
|
181
|
-
public: Boolean(action.$public),
|
|
182
|
-
inputSchema: zodToJsonSchema(action.schema),
|
|
183
|
-
emits: emits.map((e) => String(e?.name ?? "")).filter(Boolean),
|
|
184
|
-
hasInlineHandler: typeof action.handler === "object" && action.handler !== null,
|
|
185
|
-
persona: typeof action.persona === "string" ? action.persona : undefined,
|
|
186
|
-
journeyStep: typeof action.journeyStep === "string" ? action.journeyStep : undefined,
|
|
187
|
-
capability: typeof action.capability === "string" ? action.capability : undefined,
|
|
188
|
-
slo: action.slo,
|
|
189
|
-
retry: action.retry,
|
|
190
|
-
policy: action.policy,
|
|
191
|
-
tags: action.tags,
|
|
192
|
-
source: sourceOf(action),
|
|
193
|
-
});
|
|
194
|
-
for (const ev of emits) {
|
|
195
|
-
if (ev?.name && !eventNamesSeen.has(ev.name)) {
|
|
196
|
-
eventNamesSeen.add(ev.name);
|
|
197
|
-
out.events.push({
|
|
198
|
-
name: ev.name,
|
|
199
|
-
app: app.appName,
|
|
200
|
-
public: Boolean(ev.$public),
|
|
201
|
-
description: typeof ev.description === "string" ? ev.description : undefined,
|
|
202
|
-
version: typeof ev.version === "number" ? ev.version : undefined,
|
|
203
|
-
audience: Array.isArray(ev.audience) ? ev.audience : undefined,
|
|
204
|
-
source: sourceOf(ev),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
if (ev?.name) {
|
|
208
|
-
out.graph.events.push({ from: name, to: ev.name, via: "emits" });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// ── Actors ──────────────────────────────────────────────────────
|
|
213
|
-
const actors = safeMap(dispatcher.actors);
|
|
214
|
-
for (const [name, def] of actors) {
|
|
215
|
-
const stateNames = def.states && typeof def.states === "object"
|
|
216
|
-
? Object.keys(def.states)
|
|
217
|
-
: [];
|
|
218
|
-
out.actors.push({
|
|
219
|
-
name,
|
|
220
|
-
app: app.appName,
|
|
221
|
-
states: stateNames,
|
|
222
|
-
source: sourceOf(def),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
// ── Projections + listened events ──────────────────────────────
|
|
226
|
-
const projections = safeMap(dispatcher.projections);
|
|
227
|
-
for (const [name, def] of projections) {
|
|
228
|
-
const listens = (def.listens ?? []);
|
|
229
|
-
out.projections.push({
|
|
230
|
-
name,
|
|
231
|
-
app: app.appName,
|
|
232
|
-
description: typeof def.description === "string" ? def.description : undefined,
|
|
233
|
-
listens: listens.map((e) => String(e?.name ?? "")).filter(Boolean),
|
|
234
|
-
source: sourceOf(def),
|
|
235
|
-
});
|
|
236
|
-
for (const ev of listens) {
|
|
237
|
-
if (ev?.name && !eventNamesSeen.has(ev.name)) {
|
|
238
|
-
eventNamesSeen.add(ev.name);
|
|
239
|
-
out.events.push({
|
|
240
|
-
name: ev.name,
|
|
241
|
-
app: app.appName,
|
|
242
|
-
public: Boolean(ev.$public),
|
|
243
|
-
description: typeof ev.description === "string" ? ev.description : undefined,
|
|
244
|
-
version: typeof ev.version === "number" ? ev.version : undefined,
|
|
245
|
-
audience: Array.isArray(ev.audience) ? ev.audience : undefined,
|
|
246
|
-
source: sourceOf(ev),
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
if (ev?.name) {
|
|
250
|
-
out.graph.events.push({ from: ev.name, to: name, via: "folds" });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// ── Queries ─────────────────────────────────────────────────────
|
|
255
|
-
const queries = safeMap(dispatcher.queries);
|
|
256
|
-
for (const [name, def] of queries) {
|
|
257
|
-
const proj = def.projection;
|
|
258
|
-
out.queries.push({
|
|
259
|
-
name,
|
|
260
|
-
app: app.appName,
|
|
261
|
-
public: Boolean(def.$public),
|
|
262
|
-
projection: typeof proj?.name === "string" ? proj.name : undefined,
|
|
263
|
-
source: sourceOf(def),
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
// ── Workflows + subscribed/dispatched edges ────────────────────
|
|
267
|
-
const workflowDefs = dispatcher.listWorkflows?.() ?? [];
|
|
268
|
-
for (const def of workflowDefs) {
|
|
269
|
-
const name = String(def.name);
|
|
270
|
-
const subscribed = (def.subscribedEvents ?? new Set());
|
|
271
|
-
const dispatched = (def.dispatchedActions ?? new Set());
|
|
272
|
-
out.workflows.push({
|
|
273
|
-
name,
|
|
274
|
-
app: app.appName,
|
|
275
|
-
public: Boolean(def.$public),
|
|
276
|
-
description: typeof def.description === "string" ? def.description : undefined,
|
|
277
|
-
subscribesTo: [...subscribed],
|
|
278
|
-
dispatches: [...dispatched],
|
|
279
|
-
source: sourceOf(def),
|
|
280
|
-
});
|
|
281
|
-
for (const evName of (def.subscribedEvents ?? new Set())) {
|
|
282
|
-
out.graph.events.push({ from: evName, to: name, via: "subscribes" });
|
|
283
|
-
}
|
|
284
|
-
for (const actName of (def.dispatchedActions ?? new Set())) {
|
|
285
|
-
out.graph.events.push({ from: name, to: actName, via: "dispatches" });
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
// ── External calls ─────────────────────────────────────────────
|
|
289
|
-
const externalCalls = safeMap(dispatcher.externalCalls);
|
|
290
|
-
for (const [name, def] of externalCalls) {
|
|
291
|
-
out.externalCalls.push({ name, app: app.appName, source: sourceOf(def) });
|
|
292
|
-
}
|
|
293
|
-
// ── DI bindings ────────────────────────────────────────────────
|
|
294
|
-
const bindings = app.container.list?.() ?? [];
|
|
295
|
-
for (const b of bindings) {
|
|
296
|
-
out.bindings.push({
|
|
297
|
-
name: b.name,
|
|
298
|
-
kind: (b.kind === "singleton" || b.kind === "transient" ? b.kind : "singleton"),
|
|
299
|
-
app: app.appName,
|
|
300
|
-
source: b.source,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
// ── Hooks ──────────────────────────────────────────────────────
|
|
304
|
-
// Prefer the structural listHooks helper if the runtime exposes one;
|
|
305
|
-
// otherwise walk `runtime.hooks` map and read step counts off each
|
|
306
|
-
// hook. Framework slots that haven't been materialised by a plugin
|
|
307
|
-
// (`runtime.defineHook(name)`) are absent — that's correct, we don't
|
|
308
|
-
// want to surface dead slots.
|
|
309
|
-
const explicit = app.runtime?.listHooks?.();
|
|
310
|
-
if (explicit) {
|
|
311
|
-
for (const h of explicit) {
|
|
312
|
-
out.hooks.push({
|
|
313
|
-
id: h.id,
|
|
314
|
-
name: h.name,
|
|
315
|
-
chain: h.chain,
|
|
316
|
-
listeners: h.listeners,
|
|
317
|
-
source: h.source,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
else if (app.runtime?.hooks) {
|
|
322
|
-
const hookMap = app.runtime.hooks;
|
|
323
|
-
for (const [slotName, hookValue] of Object.entries(hookMap)) {
|
|
324
|
-
if (!hookValue || typeof hookValue !== "object")
|
|
325
|
-
continue;
|
|
326
|
-
const h = hookValue;
|
|
327
|
-
const counts = typeof h.stepCounts === "function" ? h.stepCounts() : { chain: 0, listeners: 0 };
|
|
328
|
-
out.hooks.push({
|
|
329
|
-
id: h.id ?? slotName,
|
|
330
|
-
name: slotName,
|
|
331
|
-
chain: counts.chain,
|
|
332
|
-
listeners: counts.listeners,
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// ── Routes (from passed-in interfaces) ────────────────────────────
|
|
338
|
-
for (const iface of options.interfaces ?? []) {
|
|
339
|
-
for (const r of iface.listRoutes?.() ?? []) {
|
|
340
|
-
out.routes.push({ method: r.method, path: r.path, action: r.action });
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// ── Resolver mounts (operator-supplied) ──────────────────────────
|
|
344
|
-
for (const m of options.mounts ?? []) {
|
|
345
|
-
out.resolvers.push({ name: m.resolverName, app: "" });
|
|
346
|
-
}
|
|
347
|
-
// ── Resources / errors / middleware (operator-supplied) ──────────
|
|
348
|
-
for (const r of options.resources ?? []) {
|
|
349
|
-
if ("definition" in r) {
|
|
350
|
-
const def = r.definition;
|
|
351
|
-
out.resources.push({
|
|
352
|
-
name: String(def?.name ?? ""),
|
|
353
|
-
app: r.app,
|
|
354
|
-
module: r.module,
|
|
355
|
-
source: sourceOf(def),
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
for (const e of options.errors ?? []) {
|
|
360
|
-
if ("definition" in e) {
|
|
361
|
-
const def = e.definition;
|
|
362
|
-
out.errors.push({
|
|
363
|
-
code: String(def?.code ?? ""),
|
|
364
|
-
app: e.app,
|
|
365
|
-
module: e.module,
|
|
366
|
-
source: sourceOf(def),
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
for (const m of options.middleware ?? []) {
|
|
371
|
-
if ("definition" in m) {
|
|
372
|
-
out.middleware.push({ name: m.definition.name, where: m.where });
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
out.middleware.push({ name: m.name });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
export async function writeCache(cache, dir) {
|
|
381
|
-
await mkdir(dir, { recursive: true });
|
|
382
|
-
await writeFile(resolve(dir, "manifest.json"), JSON.stringify(cache, null, 2));
|
|
383
|
-
}
|
|
8
|
+
// ─── Build-time manifest (static AST extract) ──────────────────────────
|
|
9
|
+
// The graph without a boot. The runtime scan above stays for now; the
|
|
10
|
+
// equivalence harness proves they match.
|
|
11
|
+
export { buildManifest, writeManifest, readManifest, collectSourceFiles, MANIFEST_VERSION, } from "./manifest.js";
|
|
12
|
+
export { extractFromFiles, collectConfigModules, } from "./ast-extract.js";
|
|
13
|
+
export { captureTopology, } from "./topology.js";
|
|
14
|
+
export { buildGraph, } from "./graph.js";
|
|
15
|
+
export { listTelemetryRuns, readTelemetryRun } from "./telemetry-runs.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk readers for past telemetry runs — the `.nwire/telemetry/*.jsonl` files
|
|
3
|
+
* the telemetry reporter writes per run.
|
|
4
|
+
*
|
|
5
|
+
* These live in `@nwire/scan` because both Studio (the standalone Vite
|
|
6
|
+
* middleware) and the `nwire` CLI's one-Vite dev host need to serve telemetry
|
|
7
|
+
* history off disk, and both already depend on `@nwire/scan`. Keeping the reader
|
|
8
|
+
* here avoids a UI-package → CLI import edge (the CLI must never depend on
|
|
9
|
+
* `@nwire/studio`) and avoids duplicating the parse/guard logic in two places.
|
|
10
|
+
*
|
|
11
|
+
* All functions are synchronous, side-effect-free beyond filesystem reads, and
|
|
12
|
+
* take an explicit project `cwd` so callers can target any root without touching
|
|
13
|
+
* process state.
|
|
14
|
+
*/
|
|
15
|
+
/** One entry from the telemetry run list — id, byte size, and last-modified. */
|
|
16
|
+
export interface TelemetryRunMeta {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
readonly mtime: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* List `.nwire/telemetry/*.jsonl` files for a project, newest first.
|
|
23
|
+
*
|
|
24
|
+
* Entry ids are the bare filename minus the `.jsonl` extension. Run ids are
|
|
25
|
+
* ISO-timestamp-prefixed by the telemetry reporter, so lexical descending order
|
|
26
|
+
* is chronological descending order. Missing dir or unreadable entries degrade
|
|
27
|
+
* to an empty list rather than throwing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function listTelemetryRuns(cwd: string): TelemetryRunMeta[];
|
|
30
|
+
/**
|
|
31
|
+
* Read `.nwire/telemetry/<runId>.jsonl` and return each line parsed as JSON.
|
|
32
|
+
*
|
|
33
|
+
* Security: `runId` must not contain path separators or `..` — the function
|
|
34
|
+
* returns an empty array for any id that fails this check. Blank lines and lines
|
|
35
|
+
* that are not valid JSON are silently skipped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readTelemetryRun(cwd: string, runId: string): unknown[];
|