@beignet/cli 0.0.39 → 0.0.41

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 (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +110 -16
  3. package/dist/analysis/source-index.d.ts +38 -0
  4. package/dist/analysis/source-index.d.ts.map +1 -0
  5. package/dist/analysis/source-index.js +271 -0
  6. package/dist/analysis/source-index.js.map +1 -0
  7. package/dist/analysis/workspace.d.ts +16 -0
  8. package/dist/analysis/workspace.d.ts.map +1 -0
  9. package/dist/analysis/workspace.js +134 -0
  10. package/dist/analysis/workspace.js.map +1 -0
  11. package/dist/app-map-schema.d.ts +5 -0
  12. package/dist/app-map-schema.d.ts.map +1 -0
  13. package/dist/app-map-schema.js +26 -0
  14. package/dist/app-map-schema.js.map +1 -0
  15. package/dist/app-map.d.ts +96 -0
  16. package/dist/app-map.d.ts.map +1 -0
  17. package/dist/app-map.js +1141 -0
  18. package/dist/app-map.js.map +1 -0
  19. package/dist/check.d.ts +7 -0
  20. package/dist/check.d.ts.map +1 -1
  21. package/dist/check.js +20 -5
  22. package/dist/check.js.map +1 -1
  23. package/dist/config.d.ts +1 -1
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +4 -2
  26. package/dist/config.js.map +1 -1
  27. package/dist/db.d.ts +32 -7
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +182 -14
  30. package/dist/db.js.map +1 -1
  31. package/dist/explain.d.ts +98 -0
  32. package/dist/explain.d.ts.map +1 -0
  33. package/dist/explain.js +512 -0
  34. package/dist/explain.js.map +1 -0
  35. package/dist/framework.d.ts +3 -0
  36. package/dist/framework.d.ts.map +1 -0
  37. package/dist/framework.js +6 -0
  38. package/dist/framework.js.map +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +79 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/inspect.d.ts +4 -2
  43. package/dist/inspect.d.ts.map +1 -1
  44. package/dist/inspect.js +173 -36
  45. package/dist/inspect.js.map +1 -1
  46. package/dist/lib.d.ts +4 -0
  47. package/dist/lib.d.ts.map +1 -1
  48. package/dist/lib.js +2 -0
  49. package/dist/lib.js.map +1 -1
  50. package/dist/make/payments.d.ts.map +1 -1
  51. package/dist/make/payments.js +2 -0
  52. package/dist/make/payments.js.map +1 -1
  53. package/dist/make.d.ts.map +1 -1
  54. package/dist/make.js +157 -3
  55. package/dist/make.js.map +1 -1
  56. package/dist/mcp.d.ts +1 -1
  57. package/dist/mcp.d.ts.map +1 -1
  58. package/dist/mcp.js +69 -1
  59. package/dist/mcp.js.map +1 -1
  60. package/dist/preflight.d.ts.map +1 -1
  61. package/dist/preflight.js +10 -0
  62. package/dist/preflight.js.map +1 -1
  63. package/dist/provider-add.d.ts.map +1 -1
  64. package/dist/provider-add.js +167 -13
  65. package/dist/provider-add.js.map +1 -1
  66. package/dist/provider-audit.d.ts +6 -0
  67. package/dist/provider-audit.d.ts.map +1 -1
  68. package/dist/provider-audit.js +63 -15
  69. package/dist/provider-audit.js.map +1 -1
  70. package/dist/templates/agents.d.ts.map +1 -1
  71. package/dist/templates/agents.js +17 -4
  72. package/dist/templates/agents.js.map +1 -1
  73. package/dist/templates/base.d.ts.map +1 -1
  74. package/dist/templates/base.js +10 -4
  75. package/dist/templates/base.js.map +1 -1
  76. package/dist/templates/index.d.ts +1 -1
  77. package/dist/templates/index.d.ts.map +1 -1
  78. package/dist/templates/index.js +8 -1
  79. package/dist/templates/index.js.map +1 -1
  80. package/dist/templates/server.d.ts +3 -0
  81. package/dist/templates/server.d.ts.map +1 -1
  82. package/dist/templates/server.js +55 -1
  83. package/dist/templates/server.js.map +1 -1
  84. package/dist/templates/shadcn.js +1 -1
  85. package/dist/templates/shared.js +1 -1
  86. package/package.json +2 -2
  87. package/skills/app-structure/SKILL.md +38 -5
  88. package/src/analysis/source-index.ts +395 -0
  89. package/src/analysis/workspace.ts +180 -0
  90. package/src/app-map-schema.ts +28 -0
  91. package/src/app-map.ts +1705 -0
  92. package/src/check.ts +27 -4
  93. package/src/config.ts +7 -3
  94. package/src/db.ts +232 -14
  95. package/src/explain.ts +786 -0
  96. package/src/framework.ts +13 -0
  97. package/src/index.ts +113 -2
  98. package/src/inspect.ts +259 -40
  99. package/src/lib.ts +36 -0
  100. package/src/make/payments.ts +7 -0
  101. package/src/make.ts +232 -2
  102. package/src/mcp.ts +107 -1
  103. package/src/preflight.ts +14 -0
  104. package/src/provider-add.ts +211 -12
  105. package/src/provider-audit.ts +127 -22
  106. package/src/templates/agents.ts +17 -4
  107. package/src/templates/base.ts +22 -4
  108. package/src/templates/index.ts +18 -1
  109. package/src/templates/server.ts +58 -1
  110. package/src/templates/shadcn.ts +1 -1
  111. package/src/templates/shared.ts +1 -1
  112. package/src/test-helpers/generated-app.ts +1 -1
@@ -0,0 +1,1141 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import { createSourceIndex, exportedVariableDeclarations, literalText, localImportReferences, propertyAssignment, propertyName, visitCalls, } from "./analysis/source-index.js";
4
+ import { createAnalysisWorkspace, } from "./analysis/workspace.js";
5
+ import { directoryPath } from "./config.js";
6
+ import { inspectApp, } from "./inspect.js";
7
+ import { lintApp } from "./lint.js";
8
+ import { auditProviders } from "./provider-audit.js";
9
+ export { appMapNodeKinds } from "./app-map-schema.js";
10
+ const workflowKinds = new Set([
11
+ "event",
12
+ "listener",
13
+ "job",
14
+ "schedule",
15
+ "task",
16
+ "notification",
17
+ "upload",
18
+ ]);
19
+ const definitionCalls = {
20
+ defineEvent: "event",
21
+ defineListener: "listener",
22
+ defineJob: "job",
23
+ defineNotificationDeliveryJob: "job",
24
+ defineSchedule: "schedule",
25
+ defineTask: "task",
26
+ defineNotification: "notification",
27
+ defineUpload: "upload",
28
+ defineAgentCapability: "agent-capability",
29
+ };
30
+ /** Build a deterministic, side-effect-free map of one Beignet app. */
31
+ export async function mapApp(options = {}) {
32
+ const workspace = await createAnalysisWorkspace(options.cwd);
33
+ const [inspection, lint, providerAudit] = await Promise.all([
34
+ inspectApp({ cwd: workspace.targetDir, strict: options.strict ?? true }),
35
+ lintApp({ cwd: workspace.targetDir }),
36
+ auditProviders({ cwd: workspace.targetDir }),
37
+ ]);
38
+ const sourceIndex = createSourceIndex(workspace);
39
+ const nodes = new Map();
40
+ const edges = [];
41
+ const unresolved = [];
42
+ const definitions = new Map();
43
+ const definitionBySource = new Map();
44
+ const registries = [];
45
+ const featuresPath = directoryPath(workspace.config.paths.features);
46
+ const featureNames = discoverFeatureNames(workspace, featuresPath);
47
+ const addNode = (node) => {
48
+ const existing = nodes.get(node.id);
49
+ if (!existing) {
50
+ nodes.set(node.id, node);
51
+ return;
52
+ }
53
+ if (sourceKey(existing.source) !== sourceKey(node.source)) {
54
+ unresolved.push({
55
+ code: "duplicate_node_id",
56
+ message: `Multiple declarations resolve to app-map node ${node.id}.`,
57
+ source: node.source,
58
+ });
59
+ }
60
+ };
61
+ const addEdge = (edge) => edges.push(edge);
62
+ for (const feature of featureNames) {
63
+ const featureFiles = filesForFeature(workspace.files, featuresPath, feature);
64
+ const roles = summarizeFeatureFiles(featureFiles);
65
+ addNode({
66
+ id: featureId(feature),
67
+ kind: "feature",
68
+ name: feature,
69
+ feature,
70
+ source: { file: `${featuresPath}/${feature}` },
71
+ status: "declared",
72
+ details: roles,
73
+ });
74
+ for (const file of featureFiles.filter(isTestFile)) {
75
+ const id = `test:${file}`;
76
+ addNode({
77
+ id,
78
+ kind: "test",
79
+ name: path.posix.basename(file),
80
+ feature,
81
+ source: { file },
82
+ status: "declared",
83
+ });
84
+ addEdge(exactEdge("contains-test", featureId(feature), id, file));
85
+ }
86
+ }
87
+ const contractIds = new Map();
88
+ const contractIdsByExport = new Map();
89
+ for (const contract of inspection.contracts) {
90
+ const id = `contract:${contract.file}#${contract.exportName}`;
91
+ const feature = featureForFile(contract.file, featuresPath);
92
+ contractIds.set(symbolKey(contract.file, contract.exportName), id);
93
+ const exports = contractIdsByExport.get(contract.exportName) ?? [];
94
+ exports.push(id);
95
+ contractIdsByExport.set(contract.exportName, exports);
96
+ addNode({
97
+ id,
98
+ kind: "contract",
99
+ name: contract.exportName,
100
+ ...(feature ? { feature } : {}),
101
+ source: { file: contract.file, exportName: contract.exportName },
102
+ status: "declared",
103
+ details: {
104
+ method: contract.method,
105
+ path: contract.path,
106
+ metadata: contract.metadata,
107
+ },
108
+ });
109
+ if (feature) {
110
+ addEdge(exactEdge("owns", featureId(feature), id, contract.file));
111
+ }
112
+ const ability = contract.metadata?.authorizationAbility;
113
+ if (ability) {
114
+ addEdge(exactEdge("authorizes", id, abilityId(ability), contract.file));
115
+ }
116
+ }
117
+ const routeIds = new Map();
118
+ for (const route of inspection.routes) {
119
+ const contractId = contractIds.get(symbolKey(route.file, route.exportName));
120
+ if (!contractId)
121
+ continue;
122
+ const id = `route:${route.file}#${route.exportName}`;
123
+ routeIds.set(contractId, id);
124
+ addNode({
125
+ id,
126
+ kind: "route",
127
+ name: `${route.method} ${route.path}`,
128
+ ...(featureForFile(route.file, featuresPath)
129
+ ? { feature: featureForFile(route.file, featuresPath) }
130
+ : {}),
131
+ source: {
132
+ file: route.handlerFile ?? route.file,
133
+ exportName: route.handlerExport,
134
+ },
135
+ status: route.handlerSource === "missing" ? "missing" : "registered",
136
+ details: {
137
+ method: route.method,
138
+ path: route.path,
139
+ handlerSource: route.handlerSource,
140
+ },
141
+ });
142
+ addEdge(exactEdge("binds", id, contractId, route.file));
143
+ if (route.handlerFile) {
144
+ const entrypointId = `entrypoint:${route.handlerFile}`;
145
+ addNode({
146
+ id: entrypointId,
147
+ kind: "entrypoint",
148
+ name: route.handlerFile,
149
+ source: { file: route.handlerFile },
150
+ status: "registered",
151
+ details: { type: route.handlerSource },
152
+ });
153
+ addEdge(exactEdge("exposes", entrypointId, id, route.handlerFile));
154
+ }
155
+ }
156
+ if (workspace.fileSet.has(workspace.config.paths.server)) {
157
+ addNode({
158
+ id: `entrypoint:${workspace.config.paths.server}`,
159
+ kind: "entrypoint",
160
+ name: workspace.config.paths.server,
161
+ source: { file: workspace.config.paths.server },
162
+ status: "registered",
163
+ details: { type: "server" },
164
+ });
165
+ }
166
+ const sourceFiles = workspace.files.filter(isTypeScriptSource);
167
+ for (const file of sourceFiles) {
168
+ const sourceFile = await workspace.readSourceFile(file);
169
+ for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
170
+ const initializer = declaration.initializer;
171
+ if (!initializer)
172
+ continue;
173
+ const useCaseCall = findCall(initializer, new Set(["command", "query"]));
174
+ if (useCaseCall) {
175
+ const runtimeName = literalText(useCaseCall.arguments[0]);
176
+ if (runtimeName) {
177
+ const kind = callMemberName(useCaseCall);
178
+ const feature = featureForFile(file, featuresPath);
179
+ const id = `use-case:${runtimeName}`;
180
+ const node = {
181
+ id,
182
+ kind: "use-case",
183
+ name: exportName,
184
+ runtimeName,
185
+ ...(feature ? { feature } : {}),
186
+ source: await sourceIndex.sourceLocation(file, declaration, exportName),
187
+ status: "declared",
188
+ details: { kind },
189
+ };
190
+ addNode(node);
191
+ const record = {
192
+ node,
193
+ file,
194
+ exportName,
195
+ declaration,
196
+ call: useCaseCall,
197
+ };
198
+ if (!definitions.has(id))
199
+ definitions.set(id, record);
200
+ definitionBySource.set(symbolKey(file, exportName), id);
201
+ if (feature) {
202
+ addEdge(exactEdge("owns", featureId(feature), id, file));
203
+ }
204
+ }
205
+ }
206
+ const definitionCall = findCall(initializer, new Set(Object.keys(definitionCalls)));
207
+ if (definitionCall) {
208
+ const call = callMemberName(definitionCall);
209
+ const kind = call
210
+ ? definitionCalls[call]
211
+ : undefined;
212
+ const runtimeName = definitionRuntimeName(call, definitionCall);
213
+ if (kind && runtimeName) {
214
+ const feature = featureForFile(file, featuresPath);
215
+ const id = `${kind}:${runtimeName}`;
216
+ const status = definitionStatus(kind, file, exportName, inspection.diagnostics);
217
+ const node = {
218
+ id,
219
+ kind,
220
+ name: exportName,
221
+ runtimeName,
222
+ ...(feature ? { feature } : {}),
223
+ source: await sourceIndex.sourceLocation(file, declaration, exportName),
224
+ status,
225
+ };
226
+ addNode(node);
227
+ if (!definitions.has(id)) {
228
+ definitions.set(id, {
229
+ node,
230
+ file,
231
+ exportName,
232
+ declaration,
233
+ call: definitionCall,
234
+ });
235
+ }
236
+ definitionBySource.set(symbolKey(file, exportName), id);
237
+ if (feature) {
238
+ addEdge(exactEdge("owns", featureId(feature), id, file));
239
+ }
240
+ }
241
+ }
242
+ const policyCall = findCall(initializer, new Set(["definePolicy"]));
243
+ if (policyCall && ts.isObjectLiteralExpression(policyCall.arguments[0])) {
244
+ const feature = featureForFile(file, featuresPath);
245
+ const policyId = `policy:${file}#${exportName}`;
246
+ addNode({
247
+ id: policyId,
248
+ kind: "policy",
249
+ name: exportName,
250
+ ...(feature ? { feature } : {}),
251
+ source: await sourceIndex.sourceLocation(file, declaration, exportName),
252
+ status: "declared",
253
+ });
254
+ if (feature) {
255
+ addEdge(exactEdge("owns", featureId(feature), policyId, file));
256
+ }
257
+ for (const property of policyCall.arguments[0].properties) {
258
+ const name = property.name ? propertyName(property.name) : undefined;
259
+ if (!name)
260
+ continue;
261
+ const id = abilityId(name);
262
+ addNode({
263
+ id,
264
+ kind: "ability",
265
+ name,
266
+ runtimeName: name,
267
+ ...(feature ? { feature } : {}),
268
+ source: await sourceIndex.sourceLocation(file, property),
269
+ status: "declared",
270
+ });
271
+ addEdge(exactEdge("registers", policyId, id, file));
272
+ }
273
+ }
274
+ const registry = await registryRecord({
275
+ workspace,
276
+ sourceIndex,
277
+ file,
278
+ exportName,
279
+ declaration,
280
+ });
281
+ if (registry) {
282
+ addNode(registry.node);
283
+ registries.push(registry);
284
+ }
285
+ }
286
+ }
287
+ for (const definition of definitions.values()) {
288
+ if (definition.node.kind === "listener") {
289
+ const options = definition.call.arguments[1];
290
+ if (options && ts.isObjectLiteralExpression(options)) {
291
+ const event = propertyAssignment(options, "event")?.initializer;
292
+ if (event) {
293
+ const target = await sourceIndex.resolveExpression(definition.file, event);
294
+ const targetId = target
295
+ ? definitionBySource.get(symbolKey(target.file, target.exportName))
296
+ : undefined;
297
+ if (targetId) {
298
+ addEdge(await nodeEdge("listens-to", definition.node.id, targetId, definition.file, event, workspace));
299
+ }
300
+ }
301
+ }
302
+ }
303
+ visitCalls(definition.declaration, (call) => {
304
+ const name = callMemberName(call);
305
+ if (name === "authorize") {
306
+ const ability = literalText(call.arguments[0]);
307
+ if (ability) {
308
+ addEdge(sourceNodeEdge("authorizes", definition.node.id, abilityId(ability), definition.file, call));
309
+ }
310
+ }
311
+ });
312
+ const emits = findCall(definition.declaration.initializer, new Set(["emits"]));
313
+ const emitsArray = emits?.arguments[0];
314
+ if (emitsArray && ts.isArrayLiteralExpression(emitsArray)) {
315
+ for (const element of emitsArray.elements) {
316
+ if (!ts.isExpression(element))
317
+ continue;
318
+ const target = await sourceIndex.resolveExpression(definition.file, element);
319
+ const targetId = target
320
+ ? definitionBySource.get(symbolKey(target.file, target.exportName))
321
+ : undefined;
322
+ if (targetId) {
323
+ addEdge(await nodeEdge("emits", definition.node.id, targetId, definition.file, element, workspace));
324
+ }
325
+ }
326
+ }
327
+ const interactionCalls = [];
328
+ visitCalls(definition.declaration, (call) => {
329
+ const name = callMemberName(call);
330
+ if (["dispatch", "publish", "run", "send"].includes(name ?? "")) {
331
+ interactionCalls.push(call);
332
+ }
333
+ });
334
+ for (const call of interactionCalls) {
335
+ const name = callMemberName(call);
336
+ const targetExpression = name === "run" && ts.isPropertyAccessExpression(call.expression)
337
+ ? call.expression.expression
338
+ : call.arguments[0];
339
+ if (!targetExpression)
340
+ continue;
341
+ const target = await sourceIndex.resolveExpression(definition.file, targetExpression);
342
+ const targetId = target
343
+ ? definitionBySource.get(symbolKey(target.file, target.exportName))
344
+ : undefined;
345
+ if (!targetId)
346
+ continue;
347
+ const edgeKind = name === "dispatch"
348
+ ? "dispatches"
349
+ : name === "send"
350
+ ? "sends"
351
+ : name === "publish"
352
+ ? "publishes"
353
+ : "executes";
354
+ addEdge(sourceNodeEdge(edgeKind, definition.node.id, targetId, definition.file, call));
355
+ }
356
+ }
357
+ for (const registry of registries) {
358
+ for (const value of registry.values) {
359
+ const target = await sourceIndex.resolveExpression(registry.file, value);
360
+ const targetId = target
361
+ ? (definitionBySource.get(symbolKey(target.file, target.exportName)) ??
362
+ registryIdForReference(target))
363
+ : undefined;
364
+ if (targetId && nodes.has(targetId)) {
365
+ addEdge(await nodeEdge("registers", registry.node.id, targetId, registry.file, value, workspace));
366
+ const targetNode = nodes.get(targetId);
367
+ if (targetNode?.status === "declared") {
368
+ nodes.set(targetId, { ...targetNode, status: "registered" });
369
+ }
370
+ }
371
+ }
372
+ }
373
+ await addRouteGroups({
374
+ workspace,
375
+ sourceIndex,
376
+ addNode,
377
+ addEdge,
378
+ unresolved,
379
+ nodes,
380
+ contractIds,
381
+ contractIdsByExport,
382
+ routeIds,
383
+ definitionBySource,
384
+ featuresPath,
385
+ diagnostics: inspection.diagnostics,
386
+ });
387
+ await addOpenApiSurface({
388
+ workspace,
389
+ inspection,
390
+ contractIds,
391
+ addNode,
392
+ addEdge,
393
+ });
394
+ await addPorts({
395
+ workspace,
396
+ sourceIndex,
397
+ addNode,
398
+ addEdge,
399
+ });
400
+ await addTables({ workspace, sourceIndex, addNode });
401
+ await addFeatureDependencies({
402
+ workspace,
403
+ featuresPath,
404
+ addEdge,
405
+ featureNames: new Set(featureNames),
406
+ });
407
+ for (const provider of providerAudit.providers) {
408
+ const id = `provider:${provider.packageName}`;
409
+ const sourceFile = "package.json";
410
+ addNode({
411
+ id,
412
+ kind: "provider",
413
+ name: provider.packageName,
414
+ source: { file: sourceFile },
415
+ status: provider.registration === "registered"
416
+ ? provider.variants.some((variant) => variant.registration === "inactive")
417
+ ? "conditional"
418
+ : "registered"
419
+ : provider.registration === "missing"
420
+ ? "missing"
421
+ : "declared",
422
+ details: {
423
+ displayName: provider.displayName,
424
+ metadataSource: provider.metadata.source
425
+ ? `${provider.packageName}/package.json`
426
+ : undefined,
427
+ registration: provider.registration,
428
+ requiredEnv: provider.requiredEnv,
429
+ requiredTables: provider.requiredTables,
430
+ appPorts: provider.appPorts,
431
+ variants: provider.variants,
432
+ watchers: provider.watchers,
433
+ registrationSources: provider.registrationSources,
434
+ },
435
+ });
436
+ for (const port of provider.appPorts.required) {
437
+ const portNodeId = portId(port.name);
438
+ if (nodes.has(portNodeId)) {
439
+ addEdge(exactEdge("provides", id, portNodeId, sourceFile));
440
+ }
441
+ }
442
+ }
443
+ const finalNodes = [...nodes.values()].sort(compareNodes);
444
+ const nodeIds = new Set(finalNodes.map((node) => node.id));
445
+ const finalEdges = dedupeEdges(edges.filter((edge) => {
446
+ const valid = nodeIds.has(edge.from) && nodeIds.has(edge.to);
447
+ if (!valid) {
448
+ unresolved.push({
449
+ code: "dangling_edge",
450
+ message: `Could not materialize ${edge.kind} edge ${edge.from} -> ${edge.to}.`,
451
+ source: {
452
+ file: edge.evidence.file,
453
+ line: edge.evidence.line,
454
+ column: edge.evidence.column,
455
+ },
456
+ });
457
+ }
458
+ return valid;
459
+ })).sort(compareEdges);
460
+ unresolved.sort((left, right) => `${left.source.file}:${left.source.line ?? 0}:${left.code}`.localeCompare(`${right.source.file}:${right.source.line ?? 0}:${right.code}`));
461
+ return {
462
+ schemaVersion: 1,
463
+ targetDir: workspace.targetDir,
464
+ app: {
465
+ framework: workspace.config.framework,
466
+ convention: inspection.convention.kind,
467
+ ...(workspace.config.configFile
468
+ ? { configFile: workspace.config.configFile }
469
+ : {}),
470
+ },
471
+ summary: summarize(finalNodes, inspection.diagnostics, lint.diagnostics, unresolved),
472
+ nodes: finalNodes,
473
+ edges: finalEdges,
474
+ diagnostics: {
475
+ doctor: inspection.diagnostics,
476
+ lint: lint.diagnostics,
477
+ },
478
+ unresolved,
479
+ };
480
+ }
481
+ /** Select a token-bounded view of a full app map. */
482
+ export function projectAppMap(result, options = {}) {
483
+ const requestedKinds = options.kinds ? new Set(options.kinds) : undefined;
484
+ let nodes = result.nodes.filter((node) => (!options.feature ||
485
+ node.feature === options.feature ||
486
+ node.id === featureId(options.feature)) &&
487
+ (!requestedKinds || requestedKinds.has(node.kind)));
488
+ const selectedIds = new Set(nodes.map((node) => node.id));
489
+ let edges = result.edges.filter((edge) => selectedIds.has(edge.from) && selectedIds.has(edge.to));
490
+ if (options.feature && !requestedKinds) {
491
+ const oneHopIds = new Set(selectedIds);
492
+ for (const edge of result.edges) {
493
+ if (selectedIds.has(edge.from))
494
+ oneHopIds.add(edge.to);
495
+ if (selectedIds.has(edge.to))
496
+ oneHopIds.add(edge.from);
497
+ }
498
+ nodes = result.nodes.filter((node) => oneHopIds.has(node.id));
499
+ edges = result.edges.filter((edge) => oneHopIds.has(edge.from) && oneHopIds.has(edge.to));
500
+ }
501
+ const files = new Set(nodes.map((node) => node.source.file));
502
+ const doctor = options.includeDiagnostics === false
503
+ ? []
504
+ : result.diagnostics.doctor.filter((diagnostic) => !diagnostic.file ||
505
+ files.has(diagnostic.file) ||
506
+ nodes.some((node) => diagnostic.message.includes(node.source.file)));
507
+ const lint = options.includeDiagnostics === false
508
+ ? []
509
+ : result.diagnostics.lint.filter((diagnostic) => files.has(diagnostic.file));
510
+ const unresolved = result.unresolved.filter((item) => files.has(item.source.file));
511
+ return {
512
+ ...result,
513
+ summary: summarize(nodes, doctor, lint, unresolved),
514
+ nodes,
515
+ edges,
516
+ diagnostics: { doctor, lint },
517
+ unresolved,
518
+ };
519
+ }
520
+ /** Format a compact human-readable inventory of a Beignet app map. */
521
+ export function formatAppMap(result) {
522
+ const featureRows = result.nodes
523
+ .filter((node) => node.kind === "feature")
524
+ .map((feature) => {
525
+ const ownedIds = new Set(result.edges
526
+ .filter((edge) => edge.kind === "owns" && edge.from === feature.id)
527
+ .map((edge) => edge.to));
528
+ const owned = result.nodes.filter((node) => ownedIds.has(node.id));
529
+ const details = feature.details ?? {};
530
+ return [
531
+ feature.name,
532
+ String(owned.filter((node) => node.kind === "contract").length),
533
+ String(owned.filter((node) => node.kind === "use-case").length),
534
+ String(owned.filter((node) => workflowKinds.has(node.kind)).length),
535
+ String(owned.filter((node) => node.kind === "port").length),
536
+ Number(details.components ?? 0) > 0 ? "yes" : "no",
537
+ String(details.tests ?? 0),
538
+ ];
539
+ });
540
+ return [
541
+ `Beignet app map for ${result.targetDir}`,
542
+ `Framework: ${result.app.framework} (${result.app.convention})`,
543
+ `Features: ${result.summary.features} Routes: ${result.summary.routes} Workflows: ${result.summary.workflows} Providers: ${result.summary.providers} OpenAPI: ${result.summary.openApiDocuments}`,
544
+ "",
545
+ ...(featureRows.length > 0
546
+ ? [
547
+ formatTable([
548
+ [
549
+ "FEATURE",
550
+ "HTTP",
551
+ "USE CASES",
552
+ "WORKFLOWS",
553
+ "PORTS",
554
+ "UI",
555
+ "TESTS",
556
+ ],
557
+ ...featureRows,
558
+ ]),
559
+ ]
560
+ : ["No feature folders found."]),
561
+ "",
562
+ `Diagnostics: ${result.summary.diagnostics} Unresolved references: ${result.summary.unresolved}`,
563
+ "Run beignet map --json for the complete graph.",
564
+ ].join("\n");
565
+ }
566
+ async function addRouteGroups(args) {
567
+ const routeFiles = args.workspace.files.filter((file) => featureForFile(file, args.featuresPath) !== undefined &&
568
+ file.endsWith("/routes.ts"));
569
+ for (const file of routeFiles) {
570
+ const sourceFile = await args.workspace.readSourceFile(file);
571
+ for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
572
+ const call = declaration.initializer
573
+ ? findCall(declaration.initializer, new Set(["defineRouteGroup"]))
574
+ : undefined;
575
+ const options = call?.arguments[0];
576
+ if (!call || !options || !ts.isObjectLiteralExpression(options))
577
+ continue;
578
+ const feature = featureForFile(file, args.featuresPath);
579
+ const groupName = literalText(propertyAssignment(options, "name")?.initializer);
580
+ const id = `route-group:${file}#${exportName}`;
581
+ const missing = args.diagnostics.some((diagnostic) => diagnostic.code === "BEIGNET_ROUTE_GROUP_UNREGISTERED" &&
582
+ diagnostic.message.includes(file) &&
583
+ diagnostic.message.includes(exportName));
584
+ args.addNode({
585
+ id,
586
+ kind: "route-group",
587
+ name: groupName ?? exportName,
588
+ ...(feature ? { feature } : {}),
589
+ source: await args.sourceIndex.sourceLocation(file, declaration, exportName),
590
+ status: missing ? "missing" : "registered",
591
+ });
592
+ if (feature) {
593
+ args.addEdge(exactEdge("owns", featureId(feature), id, file));
594
+ }
595
+ const routes = propertyAssignment(options, "routes")?.initializer;
596
+ if (!routes || !ts.isArrayLiteralExpression(routes))
597
+ continue;
598
+ for (const element of routes.elements) {
599
+ if (!ts.isObjectLiteralExpression(element)) {
600
+ args.unresolved.push({
601
+ code: "dynamic_route_entry",
602
+ message: `${exportName} contains a route entry that cannot be resolved statically.`,
603
+ source: await args.sourceIndex.sourceLocation(file, element),
604
+ });
605
+ continue;
606
+ }
607
+ const contractExpression = propertyAssignment(element, "contract")?.initializer;
608
+ if (!contractExpression)
609
+ continue;
610
+ const contractReference = await args.sourceIndex.resolveExpression(file, contractExpression);
611
+ const contractId = contractReference
612
+ ? (args.contractIds.get(symbolKey(contractReference.file, contractReference.exportName)) ??
613
+ uniqueValue(args.contractIdsByExport.get(contractReference.exportName)))
614
+ : undefined;
615
+ if (!contractId) {
616
+ args.unresolved.push({
617
+ code: "route_contract_unresolved",
618
+ message: `Could not resolve a contract reference in ${exportName}.`,
619
+ source: await args.sourceIndex.sourceLocation(file, contractExpression),
620
+ });
621
+ continue;
622
+ }
623
+ const routeId = args.routeIds.get(contractId);
624
+ if (routeId) {
625
+ args.addEdge(await nodeEdge("registers", id, routeId, file, contractExpression, args.workspace));
626
+ }
627
+ const useCaseExpression = propertyAssignment(element, "useCase")?.initializer;
628
+ if (!useCaseExpression || !routeId)
629
+ continue;
630
+ const useCaseReference = await args.sourceIndex.resolveExpression(file, useCaseExpression);
631
+ const useCaseId = useCaseReference
632
+ ? args.definitionBySource.get(symbolKey(useCaseReference.file, useCaseReference.exportName))
633
+ : undefined;
634
+ if (useCaseId) {
635
+ args.addEdge(await nodeEdge("executes", routeId, useCaseId, file, useCaseExpression, args.workspace));
636
+ }
637
+ else {
638
+ args.unresolved.push({
639
+ code: "route_use_case_unresolved",
640
+ message: `Could not resolve the use case for a route in ${exportName}.`,
641
+ source: await args.sourceIndex.sourceLocation(file, useCaseExpression),
642
+ });
643
+ }
644
+ }
645
+ }
646
+ }
647
+ }
648
+ async function addPorts(args) {
649
+ const portsFile = args.workspace.config.paths.ports;
650
+ if (!args.workspace.fileSet.has(portsFile))
651
+ return;
652
+ const sourceFile = await args.workspace.readSourceFile(portsFile);
653
+ const aliases = new Map();
654
+ for (const statement of sourceFile.statements) {
655
+ if (ts.isTypeAliasDeclaration(statement)) {
656
+ aliases.set(statement.name.text, statement.type);
657
+ }
658
+ }
659
+ const appPorts = aliases.get("AppPorts");
660
+ if (!appPorts)
661
+ return;
662
+ const bindings = await readPortBindings(args.workspace);
663
+ const properties = collectTypeProperties(appPorts, aliases);
664
+ for (const property of properties) {
665
+ const name = propertyName(property.name);
666
+ if (!name)
667
+ continue;
668
+ const feature = await portFeature(property.type, portsFile, args.workspace.config.paths.features, args.sourceIndex);
669
+ const id = portId(name);
670
+ args.addNode({
671
+ id,
672
+ kind: "port",
673
+ name,
674
+ ...(feature ? { feature } : {}),
675
+ source: await args.sourceIndex.sourceLocation(portsFile, property),
676
+ status: bindings.bound.has(name) || bindings.deferred.has(name)
677
+ ? "registered"
678
+ : "missing",
679
+ details: {
680
+ binding: bindings.bound.has(name)
681
+ ? "bound"
682
+ : bindings.deferred.has(name)
683
+ ? "deferred"
684
+ : "unresolved",
685
+ type: property.type?.getText(sourceFile),
686
+ },
687
+ });
688
+ if (feature) {
689
+ args.addEdge(exactEdge("owns", featureId(feature), id, portsFile));
690
+ }
691
+ if (bindings.bound.has(name)) {
692
+ const bindingId = `provider:app:${name}`;
693
+ args.addNode({
694
+ id: bindingId,
695
+ kind: "provider",
696
+ name: `${name} app binding`,
697
+ source: { file: args.workspace.config.paths.portWiring },
698
+ status: "registered",
699
+ details: { sourceType: "app-binding" },
700
+ });
701
+ args.addEdge(exactEdge("provides", bindingId, id, args.workspace.config.paths.portWiring));
702
+ }
703
+ }
704
+ }
705
+ async function addOpenApiSurface(args) {
706
+ const file = args.workspace.config.paths.openapiRoute;
707
+ if (!args.workspace.fileSet.has(file))
708
+ return;
709
+ const drift = args.inspection.diagnostics.filter((diagnostic) => diagnostic.file === file && diagnostic.code.includes("OPENAPI"));
710
+ const omittedContracts = new Set(drift
711
+ .filter((diagnostic) => diagnostic.code === "BEIGNET_OPENAPI_MISSING")
712
+ .flatMap((diagnostic) => diagnostic.contract ? [diagnostic.contract] : []));
713
+ const explicitlyDocumented = new Set(drift
714
+ .filter((diagnostic) => diagnostic.code === "BEIGNET_OPENAPI_UNROUTED")
715
+ .flatMap((diagnostic) => diagnostic.contract ? [diagnostic.contract] : []));
716
+ const documented = args.inspection.contracts.filter((contract) => (args.inspection.routes.some((route) => route.file === contract.file &&
717
+ route.exportName === contract.exportName) &&
718
+ !omittedContracts.has(contract.exportName)) ||
719
+ explicitlyDocumented.has(contract.exportName));
720
+ const id = `openapi:${file}`;
721
+ args.addNode({
722
+ id,
723
+ kind: "openapi",
724
+ name: "OpenAPI document",
725
+ source: { file },
726
+ status: drift.length > 0 ? "conditional" : "registered",
727
+ details: {
728
+ contracts: documented.length,
729
+ drift: drift.map((diagnostic) => diagnostic.code),
730
+ },
731
+ });
732
+ for (const contract of documented) {
733
+ const contractId = args.contractIds.get(symbolKey(contract.file, contract.exportName));
734
+ if (contractId) {
735
+ args.addEdge({
736
+ kind: "exposes",
737
+ from: id,
738
+ to: contractId,
739
+ evidence: { file, confidence: "partial" },
740
+ details: { inferredFrom: "registered-route-surface" },
741
+ });
742
+ }
743
+ }
744
+ }
745
+ async function addTables(args) {
746
+ const schemaSources = args.workspace.config.database.schemaSources;
747
+ const infraDir = directoryPath(path.dirname(args.workspace.config.paths.portWiring));
748
+ const schemaPrefix = `${infraDir}/db/schema/`;
749
+ const files = args.workspace.files.filter((file) => isTypeScriptSource(file) &&
750
+ (file.startsWith(schemaPrefix) ||
751
+ schemaSources.some((source) => file.startsWith(staticPathPrefix(source)))));
752
+ for (const file of files) {
753
+ const sourceFile = await args.workspace.readSourceFile(file);
754
+ for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
755
+ const call = declaration.initializer
756
+ ? findCall(declaration.initializer, new Set(["sqliteTable", "pgTable", "mysqlTable"]))
757
+ : undefined;
758
+ const tableName = call ? literalText(call.arguments[0]) : undefined;
759
+ if (!call || !tableName)
760
+ continue;
761
+ args.addNode({
762
+ id: `table:${tableName}`,
763
+ kind: "table",
764
+ name: tableName,
765
+ runtimeName: tableName,
766
+ source: await args.sourceIndex.sourceLocation(file, declaration, exportName),
767
+ status: "declared",
768
+ details: { dialect: callMemberName(call) },
769
+ });
770
+ }
771
+ }
772
+ }
773
+ async function addFeatureDependencies(args) {
774
+ const dependencies = new Map();
775
+ for (const file of args.workspace.files.filter(isTypeScriptSource)) {
776
+ const from = featureForFile(file, args.featuresPath);
777
+ if (!from || !args.featureNames.has(from))
778
+ continue;
779
+ for (const reference of await localImportReferences(args.workspace, file)) {
780
+ const to = featureForFile(reference.resolvedFile, args.featuresPath);
781
+ if (!to || to === from || !args.featureNames.has(to))
782
+ continue;
783
+ const key = `${from}->${to}`;
784
+ const current = dependencies.get(key) ?? {
785
+ from,
786
+ to,
787
+ file,
788
+ line: reference.line,
789
+ column: reference.column,
790
+ valueImports: 0,
791
+ typeImports: 0,
792
+ files: new Set(),
793
+ };
794
+ if (reference.kind === "type")
795
+ current.typeImports += 1;
796
+ else
797
+ current.valueImports += 1;
798
+ current.files.add(file);
799
+ dependencies.set(key, current);
800
+ }
801
+ }
802
+ for (const dependency of dependencies.values()) {
803
+ args.addEdge({
804
+ kind: "depends-on",
805
+ from: featureId(dependency.from),
806
+ to: featureId(dependency.to),
807
+ evidence: {
808
+ file: dependency.file,
809
+ line: dependency.line,
810
+ column: dependency.column,
811
+ confidence: "exact",
812
+ },
813
+ details: {
814
+ valueImports: dependency.valueImports,
815
+ typeImports: dependency.typeImports,
816
+ files: [...dependency.files].sort(),
817
+ },
818
+ });
819
+ }
820
+ }
821
+ async function registryRecord(args) {
822
+ const initializer = args.declaration.initializer;
823
+ if (!initializer)
824
+ return undefined;
825
+ const values = registryValues(initializer, args.exportName);
826
+ if (!values)
827
+ return undefined;
828
+ const feature = featureForFile(args.file, directoryPath(args.workspace.config.paths.features));
829
+ return {
830
+ file: args.file,
831
+ declaration: args.declaration,
832
+ values,
833
+ node: {
834
+ id: registryId(args.file, args.exportName),
835
+ kind: "registry",
836
+ name: args.exportName,
837
+ ...(feature ? { feature } : {}),
838
+ source: await args.sourceIndex.sourceLocation(args.file, args.declaration, args.exportName),
839
+ status: "registered",
840
+ },
841
+ };
842
+ }
843
+ function registryValues(initializer, exportName) {
844
+ const expression = unwrapExpression(initializer);
845
+ if (ts.isArrayLiteralExpression(expression) && isRegistryName(exportName)) {
846
+ return arrayExpressions(expression);
847
+ }
848
+ if (!ts.isCallExpression(expression))
849
+ return undefined;
850
+ const name = callMemberName(expression);
851
+ if (name === "defineTasks" ||
852
+ name === "defineNotificationRegistry" ||
853
+ name === "defineAgentCapabilityRegistry") {
854
+ const first = expression.arguments[0];
855
+ return first && ts.isArrayLiteralExpression(unwrapExpression(first))
856
+ ? arrayExpressions(unwrapExpression(first))
857
+ : [];
858
+ }
859
+ if (name === "defineOutboxRegistry" &&
860
+ ts.isObjectLiteralExpression(expression.arguments[0])) {
861
+ const object = expression.arguments[0];
862
+ return ["events", "jobs"].flatMap((property) => {
863
+ const value = propertyAssignment(object, property)?.initializer;
864
+ return value && ts.isArrayLiteralExpression(unwrapExpression(value))
865
+ ? arrayExpressions(unwrapExpression(value))
866
+ : [];
867
+ });
868
+ }
869
+ if (name === "defineUploads" &&
870
+ ts.isObjectLiteralExpression(expression.arguments[0])) {
871
+ return expression.arguments[0].properties.flatMap((property) => ts.isPropertyAssignment(property) ? [property.initializer] : []);
872
+ }
873
+ return undefined;
874
+ }
875
+ function arrayExpressions(array) {
876
+ return array.elements.flatMap((element) => {
877
+ if (ts.isSpreadElement(element))
878
+ return [element.expression];
879
+ return ts.isExpression(element) ? [element] : [];
880
+ });
881
+ }
882
+ function collectTypeProperties(type, aliases, visited = new Set()) {
883
+ if (ts.isTypeLiteralNode(type)) {
884
+ return type.members.filter(ts.isPropertySignature);
885
+ }
886
+ if (ts.isIntersectionTypeNode(type)) {
887
+ return type.types.flatMap((member) => collectTypeProperties(member, aliases, visited));
888
+ }
889
+ if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
890
+ const name = type.typeName.text;
891
+ if (visited.has(name))
892
+ return [];
893
+ const target = aliases.get(name);
894
+ if (!target)
895
+ return [];
896
+ visited.add(name);
897
+ return collectTypeProperties(target, aliases, visited);
898
+ }
899
+ return [];
900
+ }
901
+ async function readPortBindings(workspace) {
902
+ const result = { bound: new Set(), deferred: new Set() };
903
+ const file = workspace.config.paths.portWiring;
904
+ if (!workspace.fileSet.has(file))
905
+ return result;
906
+ const sourceFile = await workspace.readSourceFile(file);
907
+ visitCalls(sourceFile, (call) => {
908
+ const object = call.arguments.find(ts.isObjectLiteralExpression);
909
+ if (!object?.properties.some((property) => property.name !== undefined &&
910
+ propertyName(property.name) === "bound")) {
911
+ return;
912
+ }
913
+ const bound = propertyAssignment(object, "bound")?.initializer;
914
+ if (bound && ts.isObjectLiteralExpression(bound)) {
915
+ for (const property of bound.properties) {
916
+ const name = property.name ? propertyName(property.name) : undefined;
917
+ if (name)
918
+ result.bound.add(name);
919
+ }
920
+ }
921
+ const deferred = propertyAssignment(object, "deferred")?.initializer;
922
+ if (deferred && ts.isArrayLiteralExpression(deferred)) {
923
+ for (const element of deferred.elements) {
924
+ const name = literalText(element);
925
+ if (name)
926
+ result.deferred.add(name);
927
+ }
928
+ }
929
+ });
930
+ return result;
931
+ }
932
+ function discoverFeatureNames(workspace, featuresPath) {
933
+ const names = new Set();
934
+ const prefix = `${featuresPath}/`;
935
+ for (const file of workspace.files) {
936
+ if (!file.startsWith(prefix))
937
+ continue;
938
+ const feature = file.slice(prefix.length).split("/")[0];
939
+ if (feature && feature !== "shared")
940
+ names.add(feature);
941
+ }
942
+ return [...names].sort();
943
+ }
944
+ function filesForFeature(files, featuresPath, feature) {
945
+ const prefix = `${featuresPath}/${feature}/`;
946
+ return files.filter((file) => file.startsWith(prefix));
947
+ }
948
+ function summarizeFeatureFiles(files) {
949
+ return {
950
+ files: files.length,
951
+ client: files.filter((file) => file.includes("/client/")).length,
952
+ components: files.filter((file) => file.includes("/components/")).length,
953
+ domain: files.filter((file) => file.includes("/domain/")).length,
954
+ tests: files.filter(isTestFile).length,
955
+ };
956
+ }
957
+ function featureForFile(file, featuresPath) {
958
+ const prefix = `${featuresPath}/`;
959
+ if (!file.startsWith(prefix))
960
+ return undefined;
961
+ const feature = file.slice(prefix.length).split("/")[0];
962
+ return feature && feature !== "shared" ? feature : undefined;
963
+ }
964
+ async function portFeature(type, file, featuresPath, sourceIndex) {
965
+ if (!type ||
966
+ !ts.isTypeReferenceNode(type) ||
967
+ !ts.isIdentifier(type.typeName)) {
968
+ return undefined;
969
+ }
970
+ const target = await sourceIndex.resolveName(file, type.typeName.text);
971
+ return target
972
+ ? featureForFile(target.file, directoryPath(featuresPath))
973
+ : undefined;
974
+ }
975
+ function definitionStatus(kind, file, exportName, diagnostics) {
976
+ const codes = {
977
+ listener: ["BEIGNET_LISTENER_UNREGISTERED"],
978
+ schedule: ["BEIGNET_SCHEDULE_UNREGISTERED"],
979
+ task: ["BEIGNET_TASK_UNREGISTERED"],
980
+ event: ["BEIGNET_OUTBOX_EVENT_UNREGISTERED"],
981
+ job: ["BEIGNET_OUTBOX_JOB_UNREGISTERED"],
982
+ };
983
+ const relevant = codes[kind];
984
+ if (!relevant)
985
+ return "declared";
986
+ return diagnostics.some((diagnostic) => relevant.includes(diagnostic.code) &&
987
+ diagnostic.message.includes(file) &&
988
+ diagnostic.message.includes(exportName))
989
+ ? "missing"
990
+ : "registered";
991
+ }
992
+ function definitionRuntimeName(callName, call) {
993
+ if (callName !== "defineNotificationDeliveryJob") {
994
+ return literalText(call.arguments[0]);
995
+ }
996
+ const options = call.arguments[0];
997
+ return options && ts.isObjectLiteralExpression(options)
998
+ ? (literalText(propertyAssignment(options, "name")?.initializer) ??
999
+ "notifications.deliver")
1000
+ : "notifications.deliver";
1001
+ }
1002
+ function findCall(node, names) {
1003
+ if (!node)
1004
+ return undefined;
1005
+ let found;
1006
+ visitCalls(node, (call) => {
1007
+ if (!found && names.has(callMemberName(call) ?? ""))
1008
+ found = call;
1009
+ });
1010
+ return found;
1011
+ }
1012
+ function callMemberName(call) {
1013
+ if (ts.isIdentifier(call.expression))
1014
+ return call.expression.text;
1015
+ if (ts.isPropertyAccessExpression(call.expression)) {
1016
+ return call.expression.name.text;
1017
+ }
1018
+ return undefined;
1019
+ }
1020
+ function unwrapExpression(expression) {
1021
+ if (ts.isAsExpression(expression) ||
1022
+ ts.isTypeAssertionExpression(expression) ||
1023
+ ts.isParenthesizedExpression(expression) ||
1024
+ ts.isSatisfiesExpression(expression)) {
1025
+ return unwrapExpression(expression.expression);
1026
+ }
1027
+ return expression;
1028
+ }
1029
+ function isRegistryName(name) {
1030
+ return (["listeners", "schedules", "tasks", "events", "jobs"].includes(name) ||
1031
+ /(Events|Jobs|Listeners|Schedules|Tasks)$/.test(name));
1032
+ }
1033
+ function registryId(file, exportName) {
1034
+ return `registry:${file}#${exportName}`;
1035
+ }
1036
+ function registryIdForReference(reference) {
1037
+ return registryId(reference.file, reference.exportName);
1038
+ }
1039
+ function featureId(feature) {
1040
+ return `feature:${feature}`;
1041
+ }
1042
+ function abilityId(ability) {
1043
+ return `ability:${ability}`;
1044
+ }
1045
+ function portId(port) {
1046
+ return `port:${port}`;
1047
+ }
1048
+ function symbolKey(file, exportName) {
1049
+ return `${file}#${exportName}`;
1050
+ }
1051
+ function sourceKey(source) {
1052
+ return `${source.file}#${source.exportName ?? ""}`;
1053
+ }
1054
+ function exactEdge(kind, from, to, file) {
1055
+ return { kind, from, to, evidence: { file, confidence: "exact" } };
1056
+ }
1057
+ async function nodeEdge(kind, from, to, file, node, workspace) {
1058
+ const sourceFile = await workspace.readSourceFile(file);
1059
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1060
+ return {
1061
+ kind,
1062
+ from,
1063
+ to,
1064
+ evidence: {
1065
+ file,
1066
+ line: position.line + 1,
1067
+ column: position.character + 1,
1068
+ confidence: "exact",
1069
+ },
1070
+ };
1071
+ }
1072
+ function sourceNodeEdge(kind, from, to, file, node) {
1073
+ const sourceFile = node.getSourceFile();
1074
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1075
+ return {
1076
+ kind,
1077
+ from,
1078
+ to,
1079
+ evidence: {
1080
+ file,
1081
+ line: position.line + 1,
1082
+ column: position.character + 1,
1083
+ confidence: "exact",
1084
+ },
1085
+ };
1086
+ }
1087
+ function isTestFile(file) {
1088
+ return /\.(test|spec)\.(ts|tsx|mts|cts)$/.test(file);
1089
+ }
1090
+ function isTypeScriptSource(file) {
1091
+ return /\.(ts|tsx|mts|cts)$/.test(file) && !file.endsWith(".d.ts");
1092
+ }
1093
+ function staticPathPrefix(pattern) {
1094
+ const wildcard = pattern.search(/[?*[{]/);
1095
+ return wildcard === -1 ? pattern : pattern.slice(0, wildcard);
1096
+ }
1097
+ function uniqueValue(values) {
1098
+ return values?.length === 1 ? values[0] : undefined;
1099
+ }
1100
+ function summarize(nodes, doctor, lint, unresolved) {
1101
+ const count = (kind) => nodes.filter((node) => node.kind === kind).length;
1102
+ return {
1103
+ features: count("feature"),
1104
+ contracts: count("contract"),
1105
+ routes: count("route"),
1106
+ useCases: count("use-case"),
1107
+ workflows: nodes.filter((node) => workflowKinds.has(node.kind)).length,
1108
+ ports: count("port"),
1109
+ providers: count("provider"),
1110
+ openApiDocuments: count("openapi"),
1111
+ agentCapabilities: count("agent-capability"),
1112
+ diagnostics: doctor.length + lint.length,
1113
+ unresolved: unresolved.length,
1114
+ };
1115
+ }
1116
+ function dedupeEdges(edges) {
1117
+ const seen = new Set();
1118
+ return edges.filter((edge) => {
1119
+ const key = `${edge.kind}:${edge.from}:${edge.to}:${JSON.stringify(edge.details ?? {})}`;
1120
+ if (seen.has(key))
1121
+ return false;
1122
+ seen.add(key);
1123
+ return true;
1124
+ });
1125
+ }
1126
+ function compareNodes(left, right) {
1127
+ return `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`);
1128
+ }
1129
+ function compareEdges(left, right) {
1130
+ return `${left.kind}:${left.from}:${left.to}`.localeCompare(`${right.kind}:${right.from}:${right.to}`);
1131
+ }
1132
+ function formatTable(rows) {
1133
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column]?.length ?? 0)));
1134
+ return rows
1135
+ .map((row) => row
1136
+ .map((cell, column) => cell.padEnd(widths[column]))
1137
+ .join(" ")
1138
+ .trimEnd())
1139
+ .join("\n");
1140
+ }
1141
+ //# sourceMappingURL=app-map.js.map