@beignet/cli 0.0.47 → 0.0.49

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 (144) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +126 -42
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +10 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +66 -6
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map.d.ts.map +1 -1
  24. package/dist/app-map.js +311 -92
  25. package/dist/app-map.js.map +1 -1
  26. package/dist/check.d.ts +8 -1
  27. package/dist/check.d.ts.map +1 -1
  28. package/dist/check.js +55 -9
  29. package/dist/check.js.map +1 -1
  30. package/dist/db.d.ts +38 -0
  31. package/dist/db.d.ts.map +1 -1
  32. package/dist/db.js +52 -0
  33. package/dist/db.js.map +1 -1
  34. package/dist/explain.js +4 -4
  35. package/dist/explain.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +73 -6
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts.map +1 -1
  40. package/dist/inspect.js +454 -170
  41. package/dist/inspect.js.map +1 -1
  42. package/dist/lib.d.ts +2 -2
  43. package/dist/lib.d.ts.map +1 -1
  44. package/dist/lib.js +1 -1
  45. package/dist/lib.js.map +1 -1
  46. package/dist/lint.d.ts.map +1 -1
  47. package/dist/lint.js +309 -487
  48. package/dist/lint.js.map +1 -1
  49. package/dist/make/inbox.d.ts.map +1 -1
  50. package/dist/make/inbox.js +12 -5
  51. package/dist/make/inbox.js.map +1 -1
  52. package/dist/make/payments.js +3 -3
  53. package/dist/make/payments.js.map +1 -1
  54. package/dist/make/tenancy.js +1 -1
  55. package/dist/make/tenancy.js.map +1 -1
  56. package/dist/make.d.ts.map +1 -1
  57. package/dist/make.js +144 -8
  58. package/dist/make.js.map +1 -1
  59. package/dist/mcp.d.ts +1 -1
  60. package/dist/mcp.d.ts.map +1 -1
  61. package/dist/mcp.js +56 -19
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/preflight.d.ts +11 -3
  64. package/dist/preflight.d.ts.map +1 -1
  65. package/dist/preflight.js +139 -16
  66. package/dist/preflight.js.map +1 -1
  67. package/dist/provider-add.d.ts.map +1 -1
  68. package/dist/provider-add.js +149 -9
  69. package/dist/provider-add.js.map +1 -1
  70. package/dist/provider-audit.d.ts +13 -0
  71. package/dist/provider-audit.d.ts.map +1 -1
  72. package/dist/provider-audit.js +122 -8
  73. package/dist/provider-audit.js.map +1 -1
  74. package/dist/registry-edits.d.ts +7 -0
  75. package/dist/registry-edits.d.ts.map +1 -1
  76. package/dist/registry-edits.js +237 -53
  77. package/dist/registry-edits.js.map +1 -1
  78. package/dist/templates/agents.d.ts.map +1 -1
  79. package/dist/templates/agents.js +29 -7
  80. package/dist/templates/agents.js.map +1 -1
  81. package/dist/templates/base.d.ts +1 -0
  82. package/dist/templates/base.d.ts.map +1 -1
  83. package/dist/templates/base.js +20 -6
  84. package/dist/templates/base.js.map +1 -1
  85. package/dist/templates/db/index.d.ts +1 -0
  86. package/dist/templates/db/index.d.ts.map +1 -1
  87. package/dist/templates/db/index.js.map +1 -1
  88. package/dist/templates/db/migration-status.d.ts +9 -0
  89. package/dist/templates/db/migration-status.d.ts.map +1 -0
  90. package/dist/templates/db/migration-status.js +117 -0
  91. package/dist/templates/db/migration-status.js.map +1 -0
  92. package/dist/templates/db/mysql.d.ts.map +1 -1
  93. package/dist/templates/db/mysql.js +23 -0
  94. package/dist/templates/db/mysql.js.map +1 -1
  95. package/dist/templates/db/postgres.d.ts.map +1 -1
  96. package/dist/templates/db/postgres.js +21 -0
  97. package/dist/templates/db/postgres.js.map +1 -1
  98. package/dist/templates/db/sqlite.d.ts.map +1 -1
  99. package/dist/templates/db/sqlite.js +53 -0
  100. package/dist/templates/db/sqlite.js.map +1 -1
  101. package/dist/templates/index.d.ts.map +1 -1
  102. package/dist/templates/index.js +5 -0
  103. package/dist/templates/index.js.map +1 -1
  104. package/dist/templates/server.d.ts.map +1 -1
  105. package/dist/templates/server.js +2 -0
  106. package/dist/templates/server.js.map +1 -1
  107. package/dist/templates/shared.d.ts +1 -0
  108. package/dist/templates/shared.d.ts.map +1 -1
  109. package/dist/templates/shared.js +4 -2
  110. package/dist/templates/shared.js.map +1 -1
  111. package/package.json +7 -6
  112. package/skills/app-structure/SKILL.md +56 -11
  113. package/src/analysis/contract-factories.ts +256 -0
  114. package/src/analysis/layers.ts +266 -0
  115. package/src/analysis/port-wiring.ts +506 -0
  116. package/src/analysis/source-index.ts +225 -57
  117. package/src/analysis/workspace.ts +115 -6
  118. package/src/app-map.ts +418 -109
  119. package/src/check.ts +86 -10
  120. package/src/db.ts +97 -0
  121. package/src/explain.ts +4 -4
  122. package/src/index.ts +108 -7
  123. package/src/inspect.ts +659 -216
  124. package/src/lib.ts +9 -1
  125. package/src/lint.ts +393 -643
  126. package/src/make/inbox.ts +12 -5
  127. package/src/make/payments.ts +3 -3
  128. package/src/make/tenancy.ts +1 -1
  129. package/src/make.ts +225 -8
  130. package/src/mcp.ts +73 -21
  131. package/src/preflight.ts +195 -16
  132. package/src/provider-add.ts +193 -9
  133. package/src/provider-audit.ts +210 -18
  134. package/src/registry-edits.ts +313 -63
  135. package/src/templates/agents.ts +29 -7
  136. package/src/templates/base.ts +21 -4
  137. package/src/templates/db/index.ts +1 -0
  138. package/src/templates/db/migration-status.ts +126 -0
  139. package/src/templates/db/mysql.ts +23 -0
  140. package/src/templates/db/postgres.ts +21 -0
  141. package/src/templates/db/sqlite.ts +53 -0
  142. package/src/templates/index.ts +5 -0
  143. package/src/templates/server.ts +2 -0
  144. package/src/templates/shared.ts +4 -2
package/src/app-map.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import path from "node:path";
2
2
  import ts from "typescript";
3
+ import {
4
+ contractBindingNames,
5
+ contractFactoryBindings,
6
+ isImportedContractDeclaration,
7
+ } from "./analysis/contract-factories.js";
8
+ import { analyzePortWiring } from "./analysis/port-wiring.js";
3
9
  import {
4
10
  createSourceIndex,
5
11
  exportedVariableDeclarations,
@@ -175,6 +181,12 @@ type RegistryRecord = {
175
181
  file: string;
176
182
  declaration: ts.VariableDeclaration;
177
183
  values: ts.Expression[];
184
+ outbox?: {
185
+ events: ts.Expression[];
186
+ jobs: ts.Expression[];
187
+ eventsIndeterminate: boolean;
188
+ jobsIndeterminate: boolean;
189
+ };
178
190
  };
179
191
 
180
192
  /** Build a deterministic, side-effect-free map of one Beignet app. */
@@ -474,6 +486,20 @@ export async function mapApp(
474
486
  if (registry) {
475
487
  addNode(registry.node);
476
488
  registries.push(registry);
489
+ if (registry.outbox?.eventsIndeterminate) {
490
+ unresolved.push({
491
+ code: "outbox_events_unresolved",
492
+ message: `Could not resolve the events entries for outbox registry ${exportName}.`,
493
+ source: registry.node.source,
494
+ });
495
+ }
496
+ if (registry.outbox?.jobsIndeterminate) {
497
+ unresolved.push({
498
+ code: "outbox_jobs_unresolved",
499
+ message: `Could not resolve the jobs entries for outbox registry ${exportName}.`,
500
+ source: registry.node.source,
501
+ });
502
+ }
477
503
  }
478
504
  }
479
505
  }
@@ -651,6 +677,18 @@ export async function mapApp(
651
677
  sourceIndex,
652
678
  addNode,
653
679
  addEdge,
680
+ unresolved,
681
+ });
682
+
683
+ await addOutboxDeliveryGraph({
684
+ workspace,
685
+ sourceIndex,
686
+ registries,
687
+ nodes,
688
+ edges,
689
+ addNode,
690
+ addEdge,
691
+ unresolved,
654
692
  });
655
693
 
656
694
  await addTables({ workspace, sourceIndex, addNode });
@@ -1021,54 +1059,62 @@ async function addPorts(args: {
1021
1059
  sourceIndex: ReturnType<typeof createSourceIndex>;
1022
1060
  addNode(node: AppMapNode): void;
1023
1061
  addEdge(edge: AppMapEdge): void;
1062
+ unresolved: AppMapUnresolvedReference[];
1024
1063
  }): Promise<void> {
1025
1064
  const portsFile = args.workspace.config.paths.ports;
1026
1065
  if (!args.workspace.fileSet.has(portsFile)) return;
1027
1066
  const sourceFile = await args.workspace.readSourceFile(portsFile);
1028
- const aliases = new Map<string, ts.TypeNode>();
1029
- for (const statement of sourceFile.statements) {
1030
- if (ts.isTypeAliasDeclaration(statement)) {
1031
- aliases.set(statement.name.text, statement.type);
1032
- }
1067
+ const analysis = await analyzePortWiring(args.workspace);
1068
+ if (analysis.declarationIndeterminate) {
1069
+ args.unresolved.push({
1070
+ code: "app_ports_declaration_unresolved",
1071
+ message: `Could not fully resolve AppPorts in ${portsFile}.`,
1072
+ source: { file: portsFile },
1073
+ });
1033
1074
  }
1034
- const appPorts = aliases.get("AppPorts");
1035
- if (!appPorts) return;
1036
-
1037
- const bindings = await readPortBindings(args.workspace);
1038
- const properties = collectTypeProperties(appPorts, aliases);
1039
- for (const property of properties) {
1040
- const name = propertyName(property.name);
1041
- if (!name) continue;
1042
- const feature = await portFeature(
1043
- property.type,
1044
- portsFile,
1045
- args.workspace.config.paths.features,
1046
- args.sourceIndex,
1047
- );
1075
+ if (analysis.indeterminate) {
1076
+ args.unresolved.push({
1077
+ code: "port_wiring_unresolved",
1078
+ message: `Could not fully resolve port bindings in ${args.workspace.config.paths.portWiring}.`,
1079
+ source: { file: args.workspace.config.paths.portWiring },
1080
+ });
1081
+ }
1082
+ for (const name of analysis.declared) {
1083
+ const property = analysis.properties.get(name);
1084
+ const feature = property
1085
+ ? await portFeature(
1086
+ property.type,
1087
+ portsFile,
1088
+ args.workspace.config.paths.features,
1089
+ args.sourceIndex,
1090
+ )
1091
+ : undefined;
1048
1092
  const id = portId(name);
1049
1093
  args.addNode({
1050
1094
  id,
1051
1095
  kind: "port",
1052
1096
  name,
1053
1097
  ...(feature ? { feature } : {}),
1054
- source: await args.sourceIndex.sourceLocation(portsFile, property),
1098
+ source: property
1099
+ ? await args.sourceIndex.sourceLocation(portsFile, property)
1100
+ : { file: args.workspace.config.paths.portWiring },
1055
1101
  status:
1056
- bindings.bound.has(name) || bindings.deferred.has(name)
1102
+ analysis.bound.has(name) || analysis.deferred.has(name)
1057
1103
  ? "registered"
1058
1104
  : "missing",
1059
1105
  details: {
1060
- binding: bindings.bound.has(name)
1106
+ binding: analysis.bound.has(name)
1061
1107
  ? "bound"
1062
- : bindings.deferred.has(name)
1108
+ : analysis.deferred.has(name)
1063
1109
  ? "deferred"
1064
1110
  : "unresolved",
1065
- type: property.type?.getText(sourceFile),
1111
+ ...(property?.type ? { type: property.type.getText(sourceFile) } : {}),
1066
1112
  },
1067
1113
  });
1068
1114
  if (feature) {
1069
1115
  args.addEdge(exactEdge("owns", featureId(feature), id, portsFile));
1070
1116
  }
1071
- if (bindings.bound.has(name)) {
1117
+ if (analysis.bound.has(name)) {
1072
1118
  const bindingId = `provider:app:${name}`;
1073
1119
  args.addNode({
1074
1120
  id: bindingId,
@@ -1090,6 +1136,182 @@ async function addPorts(args: {
1090
1136
  }
1091
1137
  }
1092
1138
 
1139
+ async function addOutboxDeliveryGraph(args: {
1140
+ workspace: AnalysisWorkspace;
1141
+ sourceIndex: ReturnType<typeof createSourceIndex>;
1142
+ registries: RegistryRecord[];
1143
+ nodes: Map<string, AppMapNode>;
1144
+ edges: AppMapEdge[];
1145
+ addNode(node: AppMapNode): void;
1146
+ addEdge(edge: AppMapEdge): void;
1147
+ unresolved: AppMapUnresolvedReference[];
1148
+ }): Promise<void> {
1149
+ const outboxRegistries = args.registries.filter(
1150
+ (registry) => registry.outbox !== undefined,
1151
+ );
1152
+ if (outboxRegistries.length === 0) return;
1153
+ const jobRegistries = outboxRegistries.filter(
1154
+ (registry) => registry.outbox && registry.outbox.jobs.length > 0,
1155
+ );
1156
+
1157
+ const jobsPortId = portId("jobs");
1158
+ if (jobRegistries.length > 0 && !args.nodes.has(jobsPortId)) {
1159
+ args.addNode({
1160
+ id: jobsPortId,
1161
+ kind: "port",
1162
+ name: "jobs",
1163
+ source: { file: args.workspace.config.paths.ports },
1164
+ status: "missing",
1165
+ details: {
1166
+ binding: "unresolved",
1167
+ requiredBy: "outbox-jobs",
1168
+ },
1169
+ });
1170
+ }
1171
+
1172
+ for (const registry of jobRegistries) {
1173
+ args.addEdge(
1174
+ exactEdge("depends-on", registry.node.id, jobsPortId, registry.file),
1175
+ );
1176
+ for (const jobId of registeredJobIds(
1177
+ registry.node.id,
1178
+ args.nodes,
1179
+ args.edges,
1180
+ )) {
1181
+ args.addEdge(exactEdge("depends-on", jobId, jobsPortId, registry.file));
1182
+ }
1183
+ }
1184
+
1185
+ const runtimeDirectories = new Set([
1186
+ directoryPath(args.workspace.config.paths.routes),
1187
+ directoryPath(args.workspace.config.paths.server),
1188
+ directoryPath(args.workspace.config.paths.outbox),
1189
+ directoryPath(args.workspace.config.paths.portWiring),
1190
+ "server",
1191
+ "infra",
1192
+ ]);
1193
+ for (const file of args.workspace.files) {
1194
+ if (
1195
+ !isTypeScriptSource(file) ||
1196
+ isTestFile(file) ||
1197
+ ![...runtimeDirectories].some(
1198
+ (directory) =>
1199
+ directory === "." ||
1200
+ file === directory ||
1201
+ file.startsWith(`${directory}/`),
1202
+ )
1203
+ ) {
1204
+ continue;
1205
+ }
1206
+ const sourceFile = await args.workspace.readSourceFile(file);
1207
+ const providerLifecycle = isProviderLifecycleSource(file, sourceFile);
1208
+ const calls: ts.CallExpression[] = [];
1209
+ visitCalls(sourceFile, (call) => {
1210
+ const name = callMemberName(call);
1211
+ if (
1212
+ name === "createOutboxDrainRoute" ||
1213
+ (name === "drainOutbox" && !providerLifecycle)
1214
+ ) {
1215
+ calls.push(call);
1216
+ }
1217
+ });
1218
+ for (const [index, call] of calls.entries()) {
1219
+ const id = `entrypoint:${file}#outbox-drain${index === 0 ? "" : `-${index + 1}`}`;
1220
+ args.addNode({
1221
+ id,
1222
+ kind: "entrypoint",
1223
+ name: `${file} outbox drain`,
1224
+ source: await args.sourceIndex.sourceLocation(file, call),
1225
+ status: "registered",
1226
+ details: { type: "outbox-drain" },
1227
+ });
1228
+
1229
+ const options = call.arguments[0];
1230
+ const registryExpression =
1231
+ options && ts.isObjectLiteralExpression(unwrapExpression(options))
1232
+ ? objectPropertyExpressionAnalysis(
1233
+ unwrapExpression(options) as ts.ObjectLiteralExpression,
1234
+ "registry",
1235
+ ).value
1236
+ : undefined;
1237
+ const reference = registryExpression
1238
+ ? await args.sourceIndex.resolveExpression(file, registryExpression)
1239
+ : undefined;
1240
+ const resolvedRegistry = reference
1241
+ ? outboxRegistries.find(
1242
+ (registry) =>
1243
+ registry.file === reference.file &&
1244
+ registry.node.source.exportName === reference.exportName,
1245
+ )
1246
+ : outboxRegistries.length === 1
1247
+ ? outboxRegistries[0]
1248
+ : undefined;
1249
+ if (resolvedRegistry) {
1250
+ args.addEdge(
1251
+ sourceNodeEdge(
1252
+ "depends-on",
1253
+ id,
1254
+ resolvedRegistry.node.id,
1255
+ file,
1256
+ registryExpression ?? call,
1257
+ ),
1258
+ );
1259
+ if (resolvedRegistry.outbox?.jobs.length) {
1260
+ args.addEdge(
1261
+ sourceNodeEdge("depends-on", id, jobsPortId, file, call),
1262
+ );
1263
+ }
1264
+ } else {
1265
+ args.unresolved.push({
1266
+ code: "outbox_drain_registry_unresolved",
1267
+ message: `Could not resolve the outbox registry used by a drain in ${file}.`,
1268
+ source: await args.sourceIndex.sourceLocation(file, call),
1269
+ });
1270
+ }
1271
+ }
1272
+ }
1273
+ }
1274
+
1275
+ function isProviderLifecycleSource(
1276
+ file: string,
1277
+ sourceFile: ts.SourceFile,
1278
+ ): boolean {
1279
+ if (
1280
+ /(^|\/)provider(s)?\.ts$/.test(file) ||
1281
+ /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
1282
+ /(^|\/)providers?\//.test(file)
1283
+ ) {
1284
+ return true;
1285
+ }
1286
+ let providerCall = false;
1287
+ visitCalls(sourceFile, (call) => {
1288
+ if (callMemberName(call) === "createProvider") providerCall = true;
1289
+ });
1290
+ return providerCall;
1291
+ }
1292
+
1293
+ function registeredJobIds(
1294
+ registryId: string,
1295
+ nodes: Map<string, AppMapNode>,
1296
+ edges: AppMapEdge[],
1297
+ ): string[] {
1298
+ const visited = new Set<string>([registryId]);
1299
+ let frontier = [registryId];
1300
+ while (frontier.length > 0) {
1301
+ const next: string[] = [];
1302
+ for (const from of frontier) {
1303
+ for (const edge of edges) {
1304
+ if (edge.kind !== "registers" || edge.from !== from) continue;
1305
+ if (visited.has(edge.to)) continue;
1306
+ visited.add(edge.to);
1307
+ next.push(edge.to);
1308
+ }
1309
+ }
1310
+ frontier = next;
1311
+ }
1312
+ return [...visited].filter((id) => nodes.get(id)?.kind === "job").sort();
1313
+ }
1314
+
1093
1315
  async function addOpenApiSurface(args: {
1094
1316
  workspace: AnalysisWorkspace;
1095
1317
  inspection: InspectAppResult;
@@ -1298,14 +1520,22 @@ async function addUnresolvedContractDeclarations(args: {
1298
1520
  }): Promise<void> {
1299
1521
  for (const file of appMapContractSourceFiles(args.workspace)) {
1300
1522
  const sourceFile = await args.workspace.readSourceFile(file);
1523
+ const factoryBindings = contractFactoryBindings(sourceFile);
1524
+ const bindingNames = contractBindingNames(sourceFile, factoryBindings);
1301
1525
  for (const { exportName, declaration } of exportedVariableDeclarations(
1302
1526
  sourceFile,
1303
1527
  )) {
1304
1528
  if (args.contractIds.has(symbolKey(file, exportName))) continue;
1305
- const call = declaration.initializer
1306
- ? findCall(declaration.initializer, new Set(["defineContract"]))
1307
- : undefined;
1308
- if (!call) continue;
1529
+ if (
1530
+ !declaration.initializer ||
1531
+ !isImportedContractDeclaration(
1532
+ declaration.initializer,
1533
+ factoryBindings,
1534
+ bindingNames,
1535
+ )
1536
+ ) {
1537
+ continue;
1538
+ }
1309
1539
 
1310
1540
  args.unresolved.push({
1311
1541
  code: "contract_declaration_unresolved",
@@ -1348,8 +1578,8 @@ async function registryRecord(args: {
1348
1578
  }): Promise<RegistryRecord | undefined> {
1349
1579
  const initializer = args.declaration.initializer;
1350
1580
  if (!initializer) return undefined;
1351
- const values = registryValues(initializer, args.exportName);
1352
- if (!values) return undefined;
1581
+ const registry = registryValues(initializer, args.exportName);
1582
+ if (!registry) return undefined;
1353
1583
  const feature = featureForFile(
1354
1584
  args.file,
1355
1585
  directoryPath(args.workspace.config.paths.features),
@@ -1357,7 +1587,8 @@ async function registryRecord(args: {
1357
1587
  return {
1358
1588
  file: args.file,
1359
1589
  declaration: args.declaration,
1360
- values,
1590
+ values: registry.values,
1591
+ ...(registry.outbox ? { outbox: registry.outbox } : {}),
1361
1592
  node: {
1362
1593
  id: registryId(args.file, args.exportName),
1363
1594
  kind: "registry",
@@ -1369,6 +1600,13 @@ async function registryRecord(args: {
1369
1600
  args.exportName,
1370
1601
  ),
1371
1602
  status: "registered",
1603
+ ...(registry.outbox
1604
+ ? {
1605
+ details: {
1606
+ registryType: "outbox",
1607
+ },
1608
+ }
1609
+ : {}),
1372
1610
  },
1373
1611
  };
1374
1612
  }
@@ -1376,10 +1614,20 @@ async function registryRecord(args: {
1376
1614
  function registryValues(
1377
1615
  initializer: ts.Expression,
1378
1616
  exportName: string,
1379
- ): ts.Expression[] | undefined {
1617
+ ):
1618
+ | {
1619
+ values: ts.Expression[];
1620
+ outbox?: {
1621
+ events: ts.Expression[];
1622
+ jobs: ts.Expression[];
1623
+ eventsIndeterminate: boolean;
1624
+ jobsIndeterminate: boolean;
1625
+ };
1626
+ }
1627
+ | undefined {
1380
1628
  const expression = unwrapExpression(initializer);
1381
1629
  if (ts.isArrayLiteralExpression(expression) && isRegistryName(exportName)) {
1382
- return arrayExpressions(expression);
1630
+ return { values: arrayExpressions(expression) };
1383
1631
  }
1384
1632
  if (!ts.isCallExpression(expression)) return undefined;
1385
1633
 
@@ -1390,100 +1638,161 @@ function registryValues(
1390
1638
  name === "defineAgentCapabilityRegistry"
1391
1639
  ) {
1392
1640
  const first = expression.arguments[0];
1393
- return first && ts.isArrayLiteralExpression(unwrapExpression(first))
1394
- ? arrayExpressions(unwrapExpression(first) as ts.ArrayLiteralExpression)
1395
- : [];
1641
+ return {
1642
+ values:
1643
+ first && ts.isArrayLiteralExpression(unwrapExpression(first))
1644
+ ? arrayExpressions(
1645
+ unwrapExpression(first) as ts.ArrayLiteralExpression,
1646
+ )
1647
+ : [],
1648
+ };
1396
1649
  }
1397
- if (
1398
- name === "defineOutboxRegistry" &&
1399
- ts.isObjectLiteralExpression(expression.arguments[0])
1400
- ) {
1401
- const object = expression.arguments[0];
1402
- return ["events", "jobs"].flatMap((property) => {
1403
- const value = propertyAssignment(object, property)?.initializer;
1404
- return value && ts.isArrayLiteralExpression(unwrapExpression(value))
1405
- ? arrayExpressions(unwrapExpression(value) as ts.ArrayLiteralExpression)
1406
- : [];
1407
- });
1650
+ if (name === "defineOutboxRegistry") {
1651
+ const initializers = localVariableInitializers(expression.getSourceFile());
1652
+ const argument = expression.arguments[0];
1653
+ const object = argument
1654
+ ? resolveRegistryExpression(argument, initializers)
1655
+ : undefined;
1656
+ if (!object || !ts.isObjectLiteralExpression(object)) {
1657
+ return {
1658
+ values: [],
1659
+ outbox: {
1660
+ events: [],
1661
+ jobs: [],
1662
+ eventsIndeterminate: true,
1663
+ jobsIndeterminate: true,
1664
+ },
1665
+ };
1666
+ }
1667
+ const valuesFor = (property: "events" | "jobs") => {
1668
+ const match = objectPropertyExpressionAnalysis(object, property);
1669
+ return {
1670
+ values: match.value
1671
+ ? registryValueExpressions(match.value, initializers)
1672
+ : [],
1673
+ indeterminate: match.indeterminate,
1674
+ };
1675
+ };
1676
+ const events = valuesFor("events");
1677
+ const jobs = valuesFor("jobs");
1678
+ const outbox = {
1679
+ events: events.values,
1680
+ jobs: jobs.values,
1681
+ eventsIndeterminate: events.indeterminate,
1682
+ jobsIndeterminate: jobs.indeterminate,
1683
+ };
1684
+ return { values: [...outbox.events, ...outbox.jobs], outbox };
1408
1685
  }
1409
1686
  if (
1410
1687
  name === "defineUploads" &&
1411
1688
  ts.isObjectLiteralExpression(expression.arguments[0])
1412
1689
  ) {
1413
- return expression.arguments[0].properties.flatMap((property) =>
1414
- ts.isPropertyAssignment(property) ? [property.initializer] : [],
1415
- );
1690
+ return {
1691
+ values: expression.arguments[0].properties.flatMap((property) =>
1692
+ ts.isPropertyAssignment(property) ? [property.initializer] : [],
1693
+ ),
1694
+ };
1416
1695
  }
1417
1696
  return undefined;
1418
1697
  }
1419
1698
 
1420
- function arrayExpressions(array: ts.ArrayLiteralExpression): ts.Expression[] {
1421
- return array.elements.flatMap((element) => {
1422
- if (ts.isSpreadElement(element)) return [element.expression];
1423
- return ts.isExpression(element) ? [element] : [];
1424
- });
1699
+ function objectPropertyExpressionAnalysis(
1700
+ object: ts.ObjectLiteralExpression,
1701
+ name: string,
1702
+ ): { value?: ts.Expression; indeterminate: boolean } {
1703
+ let value: ts.Expression | undefined;
1704
+ let indeterminate = false;
1705
+ for (const property of object.properties) {
1706
+ if (ts.isSpreadAssignment(property) || !property.name) {
1707
+ value = undefined;
1708
+ indeterminate = true;
1709
+ continue;
1710
+ }
1711
+ const propertyKey = propertyName(property.name);
1712
+ if (!propertyKey) {
1713
+ value = undefined;
1714
+ indeterminate = true;
1715
+ continue;
1716
+ }
1717
+ if (propertyKey !== name) continue;
1718
+ if (ts.isPropertyAssignment(property)) {
1719
+ value = property.initializer;
1720
+ indeterminate = false;
1721
+ } else if (ts.isShorthandPropertyAssignment(property)) {
1722
+ value = property.name;
1723
+ indeterminate = false;
1724
+ } else {
1725
+ value = undefined;
1726
+ indeterminate = true;
1727
+ }
1728
+ }
1729
+ return { ...(value ? { value } : {}), indeterminate };
1425
1730
  }
1426
1731
 
1427
- function collectTypeProperties(
1428
- type: ts.TypeNode,
1429
- aliases: Map<string, ts.TypeNode>,
1430
- visited = new Set<string>(),
1431
- ): ts.PropertySignature[] {
1432
- if (ts.isTypeLiteralNode(type)) {
1433
- return type.members.filter(ts.isPropertySignature);
1732
+ function localVariableInitializers(
1733
+ sourceFile: ts.SourceFile,
1734
+ ): Map<string, ts.Expression> {
1735
+ const initializers = new Map<string, ts.Expression>();
1736
+ for (const statement of sourceFile.statements) {
1737
+ if (!ts.isVariableStatement(statement)) continue;
1738
+ for (const declaration of statement.declarationList.declarations) {
1739
+ if (ts.isIdentifier(declaration.name) && declaration.initializer) {
1740
+ initializers.set(declaration.name.text, declaration.initializer);
1741
+ }
1742
+ }
1434
1743
  }
1435
- if (ts.isIntersectionTypeNode(type)) {
1436
- return type.types.flatMap((member) =>
1437
- collectTypeProperties(member, aliases, visited),
1744
+ return initializers;
1745
+ }
1746
+
1747
+ function resolveRegistryExpression(
1748
+ expression: ts.Expression,
1749
+ initializers: Map<string, ts.Expression>,
1750
+ visited = new Set<string>(),
1751
+ ): ts.Expression {
1752
+ const current = unwrapExpression(expression);
1753
+ if (!ts.isIdentifier(current) || visited.has(current.text)) return current;
1754
+ const initializer = initializers.get(current.text);
1755
+ if (!initializer) return current;
1756
+ const nextVisited = new Set(visited);
1757
+ nextVisited.add(current.text);
1758
+ return resolveRegistryExpression(initializer, initializers, nextVisited);
1759
+ }
1760
+
1761
+ function registryValueExpressions(
1762
+ expression: ts.Expression,
1763
+ initializers: Map<string, ts.Expression>,
1764
+ visited = new Set<string>(),
1765
+ ): ts.Expression[] {
1766
+ const current = unwrapExpression(expression);
1767
+ if (ts.isArrayLiteralExpression(current)) {
1768
+ return current.elements.flatMap((element) =>
1769
+ ts.isSpreadElement(element)
1770
+ ? registryValueExpressions(
1771
+ element.expression,
1772
+ initializers,
1773
+ new Set(visited),
1774
+ )
1775
+ : ts.isExpression(element)
1776
+ ? [element]
1777
+ : [],
1438
1778
  );
1439
1779
  }
1440
- if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
1441
- const name = type.typeName.text;
1442
- if (visited.has(name)) return [];
1443
- const target = aliases.get(name);
1444
- if (!target) return [];
1445
- visited.add(name);
1446
- return collectTypeProperties(target, aliases, visited);
1780
+ if (ts.isIdentifier(current) && !visited.has(current.text)) {
1781
+ const initializer = initializers.get(current.text);
1782
+ if (initializer) {
1783
+ const nextVisited = new Set(visited);
1784
+ nextVisited.add(current.text);
1785
+ return registryValueExpressions(initializer, initializers, nextVisited);
1786
+ }
1447
1787
  }
1448
- return [];
1788
+ return [current];
1449
1789
  }
1450
1790
 
1451
- async function readPortBindings(workspace: AnalysisWorkspace): Promise<{
1452
- bound: Set<string>;
1453
- deferred: Set<string>;
1454
- }> {
1455
- const result = { bound: new Set<string>(), deferred: new Set<string>() };
1456
- const file = workspace.config.paths.portWiring;
1457
- if (!workspace.fileSet.has(file)) return result;
1458
- const sourceFile = await workspace.readSourceFile(file);
1459
-
1460
- visitCalls(sourceFile, (call) => {
1461
- const object = call.arguments.find(ts.isObjectLiteralExpression);
1462
- if (
1463
- !object?.properties.some(
1464
- (property) =>
1465
- property.name !== undefined &&
1466
- propertyName(property.name) === "bound",
1467
- )
1468
- ) {
1469
- return;
1470
- }
1471
- const bound = propertyAssignment(object, "bound")?.initializer;
1472
- if (bound && ts.isObjectLiteralExpression(bound)) {
1473
- for (const property of bound.properties) {
1474
- const name = property.name ? propertyName(property.name) : undefined;
1475
- if (name) result.bound.add(name);
1476
- }
1477
- }
1478
- const deferred = propertyAssignment(object, "deferred")?.initializer;
1479
- if (deferred && ts.isArrayLiteralExpression(deferred)) {
1480
- for (const element of deferred.elements) {
1481
- const name = literalText(element);
1482
- if (name) result.deferred.add(name);
1483
- }
1484
- }
1791
+ function arrayExpressions(array: ts.ArrayLiteralExpression): ts.Expression[] {
1792
+ return array.elements.flatMap((element) => {
1793
+ if (ts.isSpreadElement(element)) return [element.expression];
1794
+ return ts.isExpression(element) ? [element] : [];
1485
1795
  });
1486
- return result;
1487
1796
  }
1488
1797
 
1489
1798
  function discoverFeatureNames(