@beignet/cli 0.0.48 → 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 (88) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +76 -21
  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.js +37 -3
  27. package/dist/check.js.map +1 -1
  28. package/dist/explain.js +4 -4
  29. package/dist/explain.js.map +1 -1
  30. package/dist/inspect.d.ts.map +1 -1
  31. package/dist/inspect.js +415 -168
  32. package/dist/inspect.js.map +1 -1
  33. package/dist/lint.d.ts.map +1 -1
  34. package/dist/lint.js +309 -487
  35. package/dist/lint.js.map +1 -1
  36. package/dist/make/inbox.d.ts.map +1 -1
  37. package/dist/make/inbox.js +11 -4
  38. package/dist/make/inbox.js.map +1 -1
  39. package/dist/make/payments.js +2 -2
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +144 -8
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts +1 -1
  44. package/dist/mcp.d.ts.map +1 -1
  45. package/dist/mcp.js +19 -19
  46. package/dist/mcp.js.map +1 -1
  47. package/dist/provider-add.d.ts.map +1 -1
  48. package/dist/provider-add.js +142 -7
  49. package/dist/provider-add.js.map +1 -1
  50. package/dist/registry-edits.d.ts +7 -0
  51. package/dist/registry-edits.d.ts.map +1 -1
  52. package/dist/registry-edits.js +237 -53
  53. package/dist/registry-edits.js.map +1 -1
  54. package/dist/templates/agents.d.ts.map +1 -1
  55. package/dist/templates/agents.js +16 -0
  56. package/dist/templates/agents.js.map +1 -1
  57. package/dist/templates/base.d.ts.map +1 -1
  58. package/dist/templates/base.js +9 -1
  59. package/dist/templates/base.js.map +1 -1
  60. package/dist/templates/server.d.ts.map +1 -1
  61. package/dist/templates/server.js +2 -0
  62. package/dist/templates/server.js.map +1 -1
  63. package/dist/templates/shared.d.ts +1 -0
  64. package/dist/templates/shared.d.ts.map +1 -1
  65. package/dist/templates/shared.js +3 -2
  66. package/dist/templates/shared.js.map +1 -1
  67. package/package.json +7 -6
  68. package/skills/app-structure/SKILL.md +46 -5
  69. package/src/analysis/contract-factories.ts +256 -0
  70. package/src/analysis/layers.ts +266 -0
  71. package/src/analysis/port-wiring.ts +506 -0
  72. package/src/analysis/source-index.ts +225 -57
  73. package/src/analysis/workspace.ts +115 -6
  74. package/src/app-map.ts +418 -109
  75. package/src/check.ts +51 -3
  76. package/src/explain.ts +4 -4
  77. package/src/inspect.ts +583 -215
  78. package/src/lint.ts +393 -643
  79. package/src/make/inbox.ts +11 -4
  80. package/src/make/payments.ts +2 -2
  81. package/src/make.ts +225 -8
  82. package/src/mcp.ts +19 -22
  83. package/src/provider-add.ts +186 -7
  84. package/src/registry-edits.ts +313 -63
  85. package/src/templates/agents.ts +16 -0
  86. package/src/templates/base.ts +9 -1
  87. package/src/templates/server.ts +2 -0
  88. package/src/templates/shared.ts +3 -2
package/src/inspect.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { parseProviderPackageMetadata } from "@beignet/core/providers";
4
+ import ts from "typescript";
5
+ import {
6
+ type ContractFactoryBindings,
7
+ type ContractFactoryName,
8
+ contractFactoryBindings,
9
+ isImportedContractFactoryCall,
10
+ } from "./analysis/contract-factories.js";
11
+ import { canonicalFeatureLayerPath } from "./analysis/layers.js";
12
+ import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
13
+ import { exportedVariableDeclarations } from "./analysis/source-index.js";
4
14
  import { createPainter } from "./ansi.js";
5
15
  import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
6
16
  import {
@@ -66,7 +76,8 @@ import {
66
76
  identifiersFromArrayExpression,
67
77
  insertAfterImports,
68
78
  matchingDelimiterIndex,
69
- outboxRegistryValueInfo,
79
+ outboxRegistryEntryState,
80
+ outboxRegistryIdentifiers,
70
81
  } from "./registry-edits.js";
71
82
  import {
72
83
  currentGeneratedPackageScripts,
@@ -221,6 +232,16 @@ type FeatureRouteGroup = {
221
232
  }>;
222
233
  };
223
234
 
235
+ type ReadContractsResult = {
236
+ contracts: InspectedContract[];
237
+ queryTransportDiagnostics: InspectDiagnostic[];
238
+ };
239
+
240
+ type ParsedContract = {
241
+ contract: InspectedContract;
242
+ queryTransportMissing: boolean;
243
+ };
244
+
224
245
  export async function inspectApp(
225
246
  options: InspectAppOptions = {},
226
247
  ): Promise<InspectAppResult> {
@@ -232,7 +253,8 @@ export async function inspectApp(
232
253
  ? resolveConfig(options.config)
233
254
  : await loadBeignetConfig(targetDir, files);
234
255
  const convention = inspectConvention(files, config);
235
- const contracts = await readContracts(targetDir, files, config);
256
+ const inspectedContracts = await readContracts(targetDir, files, config);
257
+ const { contracts } = inspectedContracts;
236
258
  const matchedRoutes = await inspectRouteSurface(
237
259
  targetDir,
238
260
  files,
@@ -247,6 +269,7 @@ export async function inspectApp(
247
269
  contracts,
248
270
  matchedRoutes,
249
271
  Boolean(options.strict),
272
+ inspectedContracts.queryTransportDiagnostics,
250
273
  );
251
274
 
252
275
  return {
@@ -330,7 +353,7 @@ async function buildDoctorFixPlan(
330
353
  ? resolveConfig(options.config)
331
354
  : await loadBeignetConfig(targetDir, files);
332
355
  const convention = inspectConvention(files, config);
333
- const contracts = await readContracts(targetDir, files, config);
356
+ const { contracts } = await readContracts(targetDir, files, config);
334
357
  const matchedRoutes = await inspectRouteSurface(
335
358
  targetDir,
336
359
  files,
@@ -711,7 +734,6 @@ export const productionHardeningDiagnosticCodes = new Set([
711
734
  "BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
712
735
  "BEIGNET_CORS_CREDENTIALS_WILDCARD",
713
736
  "BEIGNET_CRON_SECRET_MISSING",
714
- "BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
715
737
  "BEIGNET_ENTITLEMENTS_PORT_MISSING",
716
738
  "BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
717
739
  "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
@@ -830,16 +852,32 @@ async function readContracts(
830
852
  targetDir: string,
831
853
  files: string[],
832
854
  config: ResolvedBeignetConfig,
833
- ): Promise<InspectedContract[]> {
855
+ ): Promise<ReadContractsResult> {
834
856
  const contracts: InspectedContract[] = [];
857
+ const queryTransportDiagnostics: InspectDiagnostic[] = [];
835
858
  const contractFiles = contractSourceFiles(files, config);
836
859
 
837
860
  for (const file of contractFiles) {
838
861
  const source = await readFile(path.join(targetDir, file), "utf8");
839
- contracts.push(...parseContracts(source, file));
862
+ for (const parsed of parseContracts(source, file)) {
863
+ contracts.push(parsed.contract);
864
+ if (!parsed.queryTransportMissing) continue;
865
+
866
+ queryTransportDiagnostics.push({
867
+ severity: "error",
868
+ code: "BEIGNET_QUERY_TRANSPORT_MISSING",
869
+ file,
870
+ contract: parsed.contract.exportName,
871
+ subject: { file, exportName: parsed.contract.exportName },
872
+ message: `${parsed.contract.exportName} declares a query schema without an explicit query transport. Pass defineQueryTransport(...) as the second .query(...) argument so the client, server, and OpenAPI document share one wire format.`,
873
+ });
874
+ }
840
875
  }
841
876
 
842
- return contracts.sort(compareContracts);
877
+ return {
878
+ contracts: contracts.sort(compareContracts),
879
+ queryTransportDiagnostics,
880
+ };
843
881
  }
844
882
 
845
883
  function inspectConvention(
@@ -901,68 +939,126 @@ function hasRequirement(files: string[], requirement: string): boolean {
901
939
  return files.includes(requirement);
902
940
  }
903
941
 
904
- function parseContracts(source: string, file: string): InspectedContract[] {
905
- const contracts: InspectedContract[] = [];
906
- const groupPrefixes = parseContractGroupPrefixes(source);
907
- const groupErrorRefs = parseContractGroupErrorRefs(source);
908
- const exportRegex =
909
- /(?:^|\n)export const\s+([A-Za-z_$][\w$]*)\s*=([\s\S]*?)(?=\nexport const\s+[A-Za-z_$][\w$]*\s*=|$)/g;
942
+ function parseContracts(source: string, file: string): ParsedContract[] {
943
+ const contracts: ParsedContract[] = [];
944
+ const sourceFile = ts.createSourceFile(
945
+ file,
946
+ source,
947
+ ts.ScriptTarget.Latest,
948
+ true,
949
+ ts.ScriptKind.TS,
950
+ );
951
+ const assignments = variableAssignments(sourceFile);
952
+ const assignmentsByName = new Map(
953
+ assignments.map((assignment) => [assignment.name, assignment.expression]),
954
+ );
955
+ const factoryBindings = contractFactoryBindings(sourceFile);
910
956
 
911
- for (const exportMatch of source.matchAll(exportRegex)) {
912
- const block = exportMatch[2];
913
- const methodMatch =
914
- /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["'](?:\s*,\s*["'][^"']+["'])?\s*\)/.exec(
915
- block,
916
- );
917
- if (methodMatch) {
918
- const receiver = methodMatch[1];
919
- const pathPrefix = groupPrefixes.get(receiver) ?? "";
920
- const routePath = joinContractPaths(pathPrefix, methodMatch[3]);
957
+ for (const { exportName, declaration } of exportedVariableDeclarations(
958
+ sourceFile,
959
+ )) {
960
+ if (!declaration.initializer) continue;
961
+ const chain = resolveContractCallChain(
962
+ declaration.initializer,
963
+ assignmentsByName,
964
+ );
965
+ if (!chain) continue;
921
966
 
967
+ const directContract = parseDirectContract(chain, factoryBindings);
968
+ if (directContract) {
922
969
  contracts.push({
923
- exportName: exportMatch[1],
924
- file,
925
- metadata: parseContractMetadata(
926
- block,
927
- groupErrorRefs.get(receiver) ?? [],
928
- ),
929
- method: methodMatch[2].toUpperCase() as HttpMethod,
930
- path: routePath,
970
+ contract: {
971
+ exportName,
972
+ file,
973
+ metadata: parseContractMetadata(
974
+ applyContractMetadataCalls(
975
+ emptyContractMetadataState(),
976
+ chain.calls,
977
+ ),
978
+ ),
979
+ method: directContract.method,
980
+ path: normalizeContractPath(directContract.path),
981
+ },
982
+ queryTransportMissing: contractQueryHasTransport(chain) === false,
931
983
  });
932
984
  continue;
933
985
  }
934
986
 
935
- const directContract = parseDirectContract(block);
936
- if (directContract) {
937
- contracts.push({
938
- exportName: exportMatch[1],
987
+ const methodIndex = chain.calls.findIndex((call) =>
988
+ isHttpMethod(call.name.toUpperCase()),
989
+ );
990
+ if (methodIndex === -1) continue;
991
+ const methodCall = chain.calls[methodIndex];
992
+ const routePath = staticString(methodCall.arguments[0]);
993
+ if (!routePath) continue;
994
+
995
+ const hasGroupFactory = contractFactoryCall(
996
+ chain,
997
+ "defineContractGroup",
998
+ factoryBindings,
999
+ );
1000
+ if (!hasGroupFactory) continue;
1001
+
1002
+ const pathPrefix = applyStaticPrefixes(
1003
+ "",
1004
+ chain.calls.slice(0, methodIndex),
1005
+ );
1006
+ if (pathPrefix === undefined) continue;
1007
+
1008
+ contracts.push({
1009
+ contract: {
1010
+ exportName,
939
1011
  file,
940
- metadata: parseContractMetadata(block),
941
- method: directContract.method,
942
- path: normalizeContractPath(directContract.path),
943
- });
944
- }
1012
+ metadata: parseContractMetadata(
1013
+ applyContractMetadataCalls(emptyContractMetadataState(), chain.calls),
1014
+ ),
1015
+ method: methodCall.name.toUpperCase() as HttpMethod,
1016
+ path: joinContractPaths(pathPrefix, routePath),
1017
+ },
1018
+ queryTransportMissing: contractQueryHasTransport(chain) === false,
1019
+ });
945
1020
  }
946
1021
 
947
1022
  return contracts;
948
1023
  }
949
1024
 
1025
+ function unwrapContractExpression(expression: ts.Expression): ts.Expression {
1026
+ let current = expression;
1027
+ while (
1028
+ ts.isParenthesizedExpression(current) ||
1029
+ ts.isAsExpression(current) ||
1030
+ ts.isTypeAssertionExpression(current) ||
1031
+ ts.isNonNullExpression(current) ||
1032
+ ts.isSatisfiesExpression(current)
1033
+ ) {
1034
+ current = current.expression;
1035
+ }
1036
+ return current;
1037
+ }
1038
+
1039
+ type ContractMetadataState = {
1040
+ authRequired?: boolean;
1041
+ authorizationAbility?: string;
1042
+ authorizationAuditRequired?: boolean;
1043
+ audit?: boolean;
1044
+ auditRequired?: boolean;
1045
+ catalogErrors: InspectedCatalogErrorRef[];
1046
+ };
1047
+
1048
+ function emptyContractMetadataState(): ContractMetadataState {
1049
+ return { catalogErrors: [] };
1050
+ }
1051
+
950
1052
  function parseContractMetadata(
951
- block: string,
952
- inheritedCatalogErrors: InspectedCatalogErrorRef[] = [],
1053
+ state: ContractMetadataState,
953
1054
  ): InspectedContractMetadata | undefined {
954
- const authRequired = /\bauth\s*:\s*["']required["']/.test(block);
955
- const authorizationAbility =
956
- /\bauthorization\s*:\s*\{[\s\S]*?\bability\s*:\s*["']([^"']+)["']/.exec(
957
- block,
958
- )?.[1];
1055
+ const authRequired = state.authRequired === true;
1056
+ const authorizationAbility = state.authorizationAbility;
959
1057
  const auditRequired =
960
- /\baudit\s*:\s*["']required["']/.test(block) ||
961
- /\bauditRequired\s*:\s*true\b/.test(block);
962
- const catalogErrors = dedupeCatalogErrorRefs([
963
- ...inheritedCatalogErrors,
964
- ...parseCatalogErrorRefs(block),
965
- ]);
1058
+ state.audit === true ||
1059
+ state.auditRequired === true ||
1060
+ state.authorizationAuditRequired === true;
1061
+ const catalogErrors = state.catalogErrors;
966
1062
 
967
1063
  if (
968
1064
  !authRequired &&
@@ -981,21 +1077,129 @@ function parseContractMetadata(
981
1077
  };
982
1078
  }
983
1079
 
984
- function parseDirectContract(
985
- block: string,
986
- ): { method: HttpMethod; path: string } | undefined {
987
- const configMatch = /defineContract\s*\(\s*\{([\s\S]*?)\}\s*\)/.exec(block);
988
- if (!configMatch) return undefined;
1080
+ function applyContractMetadataCalls(
1081
+ initialState: ContractMetadataState,
1082
+ calls: ContractCall[],
1083
+ ): ContractMetadataState {
1084
+ const state: ContractMetadataState = {
1085
+ ...initialState,
1086
+ catalogErrors: [...initialState.catalogErrors],
1087
+ };
1088
+
1089
+ for (const call of calls) {
1090
+ if (call.name === "meta") {
1091
+ applyContractMetaObject(state, objectLiteral(call.arguments[0]));
1092
+ continue;
1093
+ }
1094
+ if (call.name === "errors") {
1095
+ state.catalogErrors = applyCatalogErrorCall(
1096
+ state.catalogErrors,
1097
+ call.arguments[0],
1098
+ );
1099
+ }
1100
+ }
989
1101
 
990
- const configSource = configMatch[1];
991
- const methodMatch = /method\s*:\s*["']([^"']+)["']/.exec(configSource);
992
- const pathMatch = /path\s*:\s*["']([^"']+)["']/.exec(configSource);
993
- if (!methodMatch || !pathMatch) return undefined;
1102
+ return state;
1103
+ }
994
1104
 
995
- const method = methodMatch[1].toUpperCase();
996
- if (!isHttpMethod(method)) return undefined;
1105
+ function applyContractMetaObject(
1106
+ state: ContractMetadataState,
1107
+ metadata: ts.ObjectLiteralExpression | undefined,
1108
+ ): void {
1109
+ if (!metadata) {
1110
+ state.authRequired = undefined;
1111
+ state.authorizationAbility = undefined;
1112
+ state.authorizationAuditRequired = undefined;
1113
+ state.audit = undefined;
1114
+ state.auditRequired = undefined;
1115
+ return;
1116
+ }
1117
+
1118
+ const auth = objectPropertyResult(metadata, "auth");
1119
+ if (auth.kind !== "absent") {
1120
+ state.authRequired =
1121
+ auth.kind === "value" ? staticRequiredValue(auth.value) : undefined;
1122
+ }
1123
+
1124
+ const authorization = objectPropertyResult(metadata, "authorization");
1125
+ if (authorization.kind !== "absent") {
1126
+ const value =
1127
+ authorization.kind === "value"
1128
+ ? objectLiteral(authorization.value)
1129
+ : undefined;
1130
+ if (!value) {
1131
+ state.authorizationAbility = undefined;
1132
+ state.authorizationAuditRequired = undefined;
1133
+ } else {
1134
+ const ability = objectPropertyResult(value, "ability");
1135
+ state.authorizationAbility =
1136
+ ability.kind === "value" ? staticString(ability.value) : undefined;
1137
+ state.authorizationAuditRequired = objectRequiresAudit(value);
1138
+ }
1139
+ }
997
1140
 
998
- return { method, path: pathMatch[1] };
1141
+ const audit = objectPropertyResult(metadata, "audit");
1142
+ const auditRequired = objectPropertyResult(metadata, "auditRequired");
1143
+ if (audit.kind !== "absent") {
1144
+ state.audit =
1145
+ audit.kind === "value" ? staticRequiredValue(audit.value) : undefined;
1146
+ }
1147
+ if (auditRequired.kind !== "absent") {
1148
+ state.auditRequired =
1149
+ auditRequired.kind === "value"
1150
+ ? staticBoolean(auditRequired.value)
1151
+ : undefined;
1152
+ }
1153
+ }
1154
+
1155
+ function objectRequiresAudit(
1156
+ metadata: ts.ObjectLiteralExpression,
1157
+ ): boolean | undefined {
1158
+ const audit = objectPropertyResult(metadata, "audit");
1159
+ const auditRequired = objectPropertyResult(metadata, "auditRequired");
1160
+ if (audit.kind === "unknown" || auditRequired.kind === "unknown") {
1161
+ return undefined;
1162
+ }
1163
+ const auditValue =
1164
+ audit.kind === "value" ? staticRequiredValue(audit.value) : false;
1165
+ const auditRequiredValue =
1166
+ auditRequired.kind === "value" ? staticBoolean(auditRequired.value) : false;
1167
+ if (auditValue === undefined || auditRequiredValue === undefined) {
1168
+ return undefined;
1169
+ }
1170
+ return auditValue || auditRequiredValue;
1171
+ }
1172
+
1173
+ function staticRequiredValue(expression: ts.Expression): boolean | undefined {
1174
+ const value = staticString(expression);
1175
+ return value === undefined ? undefined : value === "required";
1176
+ }
1177
+
1178
+ function staticBoolean(expression: ts.Expression): boolean | undefined {
1179
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
1180
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
1181
+ return undefined;
1182
+ }
1183
+
1184
+ function parseDirectContract(
1185
+ chain: ContractCallChain,
1186
+ factoryBindings: ContractFactoryBindings,
1187
+ ): { method: HttpMethod; path: string } | undefined {
1188
+ const factoryCall = contractFactoryCall(
1189
+ chain,
1190
+ "defineContract",
1191
+ factoryBindings,
1192
+ );
1193
+ if (!factoryCall) return undefined;
1194
+ const options = objectLiteral(factoryCall.arguments[0]);
1195
+ if (!options) return undefined;
1196
+ const methodValue = staticString(objectProperty(options, "method"));
1197
+ const contractPath = staticString(objectProperty(options, "path"));
1198
+ if (!methodValue || !contractPath) return undefined;
1199
+
1200
+ const method = methodValue.toUpperCase();
1201
+ if (!isHttpMethod(method)) return undefined;
1202
+ return { method, path: contractPath };
999
1203
  }
1000
1204
 
1001
1205
  function isHttpMethod(method: string): method is HttpMethod {
@@ -1004,118 +1208,245 @@ function isHttpMethod(method: string): method is HttpMethod {
1004
1208
  );
1005
1209
  }
1006
1210
 
1007
- function parseContractGroupPrefixes(source: string): Map<string, string> {
1008
- const assignments = [
1009
- ...source.matchAll(
1010
- /(?:^|\n)(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*([\s\S]*?);/g,
1011
- ),
1012
- ].map((match) => ({
1013
- name: match[1],
1014
- expression: match[2],
1015
- }));
1016
- const prefixes = new Map<string, string>();
1211
+ type VariableAssignment = {
1212
+ name: string;
1213
+ expression: ts.Expression;
1214
+ };
1017
1215
 
1018
- let changed = true;
1019
- while (changed) {
1020
- changed = false;
1021
- for (const assignment of assignments) {
1022
- if (prefixes.has(assignment.name)) continue;
1216
+ type ContractCall = {
1217
+ name: string;
1218
+ arguments: readonly ts.Expression[];
1219
+ node: ts.CallExpression;
1220
+ };
1023
1221
 
1024
- const basePrefix = baseGroupPrefix(assignment.expression, prefixes);
1025
- if (basePrefix === undefined) continue;
1222
+ type ContractCallChain = {
1223
+ baseIdentifier?: string;
1224
+ rootCall?: ContractCall;
1225
+ calls: ContractCall[];
1226
+ };
1026
1227
 
1027
- const prefix = prefixCalls(assignment.expression).reduce(
1028
- (current, next) => joinContractPaths(current, next),
1029
- basePrefix,
1030
- );
1031
- prefixes.set(assignment.name, prefix);
1032
- changed = true;
1228
+ function variableAssignments(sourceFile: ts.SourceFile): VariableAssignment[] {
1229
+ return sourceFile.statements
1230
+ .filter(ts.isVariableStatement)
1231
+ .flatMap((statement) =>
1232
+ [...statement.declarationList.declarations]
1233
+ .filter(
1234
+ (
1235
+ declaration,
1236
+ ): declaration is ts.VariableDeclaration & {
1237
+ name: ts.Identifier;
1238
+ initializer: ts.Expression;
1239
+ } =>
1240
+ ts.isIdentifier(declaration.name) &&
1241
+ declaration.initializer !== undefined,
1242
+ )
1243
+ .map((declaration) => ({
1244
+ name: declaration.name.text,
1245
+ expression: declaration.initializer,
1246
+ })),
1247
+ );
1248
+ }
1249
+
1250
+ function resolveContractCallChain(
1251
+ expression: ts.Expression,
1252
+ assignments: ReadonlyMap<string, ts.Expression>,
1253
+ resolving = new Set<string>(),
1254
+ ): ContractCallChain | undefined {
1255
+ const chain = contractCallChain(expression);
1256
+ if (!chain || chain.rootCall || !chain.baseIdentifier) return chain;
1257
+
1258
+ const baseExpression = assignments.get(chain.baseIdentifier);
1259
+ if (!baseExpression || resolving.has(chain.baseIdentifier)) return chain;
1260
+
1261
+ const nextResolving = new Set(resolving);
1262
+ nextResolving.add(chain.baseIdentifier);
1263
+ const baseChain = resolveContractCallChain(
1264
+ baseExpression,
1265
+ assignments,
1266
+ nextResolving,
1267
+ );
1268
+ if (!baseChain) return chain;
1269
+
1270
+ return {
1271
+ ...baseChain,
1272
+ calls: [...baseChain.calls, ...chain.calls],
1273
+ };
1274
+ }
1275
+
1276
+ function contractCallChain(
1277
+ rawExpression: ts.Expression,
1278
+ ): ContractCallChain | undefined {
1279
+ const calls: ContractCall[] = [];
1280
+ let expression = rawExpression;
1281
+
1282
+ while (true) {
1283
+ expression = unwrapContractExpression(expression);
1284
+ if (ts.isPropertyAccessExpression(expression)) {
1285
+ expression = expression.expression;
1286
+ continue;
1287
+ }
1288
+ if (ts.isCallExpression(expression)) {
1289
+ const callee = unwrapContractExpression(expression.expression);
1290
+ if (ts.isPropertyAccessExpression(callee)) {
1291
+ calls.unshift({
1292
+ name: callee.name.text,
1293
+ arguments: [...expression.arguments],
1294
+ node: expression,
1295
+ });
1296
+ expression = callee.expression;
1297
+ continue;
1298
+ }
1299
+ if (ts.isIdentifier(callee)) {
1300
+ return {
1301
+ baseIdentifier: callee.text,
1302
+ rootCall: {
1303
+ name: callee.text,
1304
+ arguments: [...expression.arguments],
1305
+ node: expression,
1306
+ },
1307
+ calls,
1308
+ };
1309
+ }
1310
+ return undefined;
1033
1311
  }
1312
+ if (ts.isIdentifier(expression)) {
1313
+ return { baseIdentifier: expression.text, calls };
1314
+ }
1315
+ return undefined;
1034
1316
  }
1317
+ }
1035
1318
 
1036
- return prefixes;
1319
+ function contractFactoryCall(
1320
+ chain: ContractCallChain,
1321
+ name: ContractFactoryName,
1322
+ factoryBindings: ContractFactoryBindings,
1323
+ ): ContractCall | undefined {
1324
+ const calls = chain.rootCall ? [chain.rootCall] : chain.calls;
1325
+ return calls.find((call) =>
1326
+ isImportedContractFactoryCall(call.node, name, factoryBindings),
1327
+ );
1037
1328
  }
1038
1329
 
1039
- function parseContractGroupErrorRefs(
1040
- source: string,
1041
- ): Map<string, InspectedCatalogErrorRef[]> {
1042
- const assignments = [
1043
- ...source.matchAll(
1044
- /(?:^|\n)(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*([\s\S]*?);/g,
1045
- ),
1046
- ].map((match) => ({
1047
- name: match[1],
1048
- expression: match[2],
1049
- }));
1050
- const groupErrors = new Map<string, InspectedCatalogErrorRef[]>();
1051
-
1052
- let changed = true;
1053
- while (changed) {
1054
- changed = false;
1055
- for (const assignment of assignments) {
1056
- if (groupErrors.has(assignment.name)) continue;
1057
-
1058
- const baseErrors = baseGroupErrors(assignment.expression, groupErrors);
1059
- if (baseErrors === undefined) continue;
1060
-
1061
- groupErrors.set(
1062
- assignment.name,
1063
- dedupeCatalogErrorRefs([
1064
- ...baseErrors,
1065
- ...parseCatalogErrorRefs(assignment.expression),
1066
- ]),
1067
- );
1068
- changed = true;
1069
- }
1330
+ function applyStaticPrefixes(
1331
+ initialPrefix: string,
1332
+ calls: ContractCall[],
1333
+ ): string | undefined {
1334
+ let prefix = initialPrefix;
1335
+ for (const call of calls) {
1336
+ if (call.name !== "prefix") continue;
1337
+ const next = staticString(call.arguments[0]);
1338
+ if (next === undefined) return undefined;
1339
+ prefix = joinContractPaths(prefix, next);
1070
1340
  }
1341
+ return prefix;
1342
+ }
1071
1343
 
1072
- return groupErrors;
1344
+ function contractQueryHasTransport(
1345
+ chain: ContractCallChain,
1346
+ ): boolean | undefined {
1347
+ const queryCall = chain.calls.find((call) => call.name === "query");
1348
+ return queryCall ? queryCall.arguments.length >= 2 : undefined;
1073
1349
  }
1074
1350
 
1075
- function baseGroupErrors(
1076
- expression: string,
1077
- groupErrors: Map<string, InspectedCatalogErrorRef[]>,
1078
- ): InspectedCatalogErrorRef[] | undefined {
1079
- if (expression.includes("defineContractGroup(")) return [];
1351
+ function applyCatalogErrorCall(
1352
+ current: InspectedCatalogErrorRef[],
1353
+ expression: ts.Expression | undefined,
1354
+ ): InspectedCatalogErrorRef[] {
1355
+ const errors = objectLiteral(expression);
1356
+ if (!errors) return [];
1357
+ let merged = [...current];
1080
1358
 
1081
- const baseMatch = /^\s*([A-Za-z_$][\w$]*)\b/.exec(expression);
1082
- if (!baseMatch) return undefined;
1359
+ for (const property of errors.properties) {
1360
+ if (ts.isSpreadAssignment(property)) {
1361
+ merged = [];
1362
+ continue;
1363
+ }
1364
+ const key = staticPropertyName(property.name);
1365
+ if (!key) {
1366
+ merged = [];
1367
+ continue;
1368
+ }
1083
1369
 
1084
- return groupErrors.get(baseMatch[1]);
1085
- }
1370
+ merged = merged.filter((ref) => ref.key !== key);
1371
+ if (!ts.isPropertyAssignment(property)) continue;
1372
+ const value = unwrapContractExpression(property.initializer);
1373
+ if (
1374
+ !ts.isPropertyAccessExpression(value) ||
1375
+ !ts.isIdentifier(value.expression)
1376
+ ) {
1377
+ continue;
1378
+ }
1379
+ merged.push({
1380
+ key,
1381
+ catalog: value.expression.text,
1382
+ catalogKey: value.name.text,
1383
+ });
1384
+ }
1086
1385
 
1087
- function baseGroupPrefix(
1088
- expression: string,
1089
- prefixes: Map<string, string>,
1090
- ): string | undefined {
1091
- if (expression.includes("defineContractGroup(")) return "";
1386
+ return merged;
1387
+ }
1092
1388
 
1093
- const baseMatch = /^\s*([A-Za-z_$][\w$]*)\b/.exec(expression);
1094
- if (!baseMatch) return undefined;
1389
+ function objectLiteral(
1390
+ expression: ts.Expression | undefined,
1391
+ ): ts.ObjectLiteralExpression | undefined {
1392
+ if (!expression) return undefined;
1393
+ const unwrapped = unwrapContractExpression(expression);
1394
+ return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : undefined;
1395
+ }
1095
1396
 
1096
- return prefixes.get(baseMatch[1]);
1397
+ function objectProperty(
1398
+ object: ts.ObjectLiteralExpression,
1399
+ name: string,
1400
+ ): ts.Expression | undefined {
1401
+ const result = objectPropertyResult(object, name);
1402
+ return result.kind === "value" ? result.value : undefined;
1097
1403
  }
1098
1404
 
1099
- function prefixCalls(expression: string): string[] {
1100
- return [...expression.matchAll(/\.prefix\(\s*["']([^"']+)["']\s*\)/g)].map(
1101
- (match) => match[1],
1102
- );
1405
+ type ObjectPropertyResult =
1406
+ | { kind: "absent" }
1407
+ | { kind: "unknown" }
1408
+ | { kind: "value"; value: ts.Expression };
1409
+
1410
+ function objectPropertyResult(
1411
+ object: ts.ObjectLiteralExpression,
1412
+ name: string,
1413
+ ): ObjectPropertyResult {
1414
+ for (const property of [...object.properties].reverse()) {
1415
+ if (ts.isSpreadAssignment(property)) return { kind: "unknown" };
1416
+ const propertyName = staticPropertyName(property.name);
1417
+ if (propertyName === undefined) return { kind: "unknown" };
1418
+ if (propertyName !== name) continue;
1419
+ if (ts.isPropertyAssignment(property)) {
1420
+ return {
1421
+ kind: "value",
1422
+ value: unwrapContractExpression(property.initializer),
1423
+ };
1424
+ }
1425
+ return { kind: "unknown" };
1426
+ }
1427
+ return { kind: "absent" };
1103
1428
  }
1104
1429
 
1105
- function parseCatalogErrorRefs(source: string): InspectedCatalogErrorRef[] {
1106
- return dedupeCatalogErrorRefs(
1107
- objectArgumentBodies(source, /\.errors\s*\(/g).flatMap((body) =>
1108
- [...body.matchAll(errorCatalogRefRegex)].map((match) => ({
1109
- key: match[1],
1110
- catalog: match[2],
1111
- catalogKey: match[3],
1112
- })),
1113
- ),
1114
- );
1430
+ function staticPropertyName(name: ts.PropertyName): string | undefined {
1431
+ if (
1432
+ ts.isIdentifier(name) ||
1433
+ ts.isStringLiteral(name) ||
1434
+ ts.isNumericLiteral(name)
1435
+ ) {
1436
+ return name.text;
1437
+ }
1438
+ return undefined;
1115
1439
  }
1116
1440
 
1117
- const errorCatalogRefRegex =
1118
- /(?:^|[,{])\s*([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)/g;
1441
+ function staticString(
1442
+ expression: ts.Expression | undefined,
1443
+ ): string | undefined {
1444
+ if (!expression) return undefined;
1445
+ const value = unwrapContractExpression(expression);
1446
+ return ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
1447
+ ? value.text
1448
+ : undefined;
1449
+ }
1119
1450
 
1120
1451
  function parseErrorCatalogKeys(source: string): Set<string> {
1121
1452
  const keys = new Set<string>();
@@ -1208,23 +1539,6 @@ function topLevelObjectKeys(body: string): string[] {
1208
1539
  return keys;
1209
1540
  }
1210
1541
 
1211
- function dedupeCatalogErrorRefs(
1212
- refs: InspectedCatalogErrorRef[],
1213
- ): InspectedCatalogErrorRef[] {
1214
- const seen = new Set<string>();
1215
- const deduped: InspectedCatalogErrorRef[] = [];
1216
-
1217
- for (const ref of refs) {
1218
- const key = `${ref.key}:${ref.catalog}:${ref.catalogKey}`;
1219
- if (seen.has(key)) continue;
1220
-
1221
- seen.add(key);
1222
- deduped.push(ref);
1223
- }
1224
-
1225
- return deduped;
1226
- }
1227
-
1228
1542
  function joinContractPaths(prefix: string, routePath: string): string {
1229
1543
  if (!prefix) return normalizeContractPath(routePath);
1230
1544
  const segments = [
@@ -1667,8 +1981,9 @@ async function inspectDiagnostics(
1667
1981
  contracts: InspectedContract[],
1668
1982
  matchedRoutes: MatchedRoutesResult,
1669
1983
  strict: boolean,
1984
+ queryTransportDiagnostics: InspectDiagnostic[],
1670
1985
  ): Promise<InspectDiagnostic[]> {
1671
- const diagnostics: InspectDiagnostic[] = [];
1986
+ const diagnostics: InspectDiagnostic[] = [...queryTransportDiagnostics];
1672
1987
  const frameworkLayout =
1673
1988
  config.framework === "next" ? convention.nextLayout : convention.webLayout;
1674
1989
  const missingFrameworkLayout =
@@ -1775,6 +2090,7 @@ async function inspectDiagnostics(
1775
2090
  contracts,
1776
2091
  )),
1777
2092
  ...(await inspectServerlessFootguns(targetDir, files, config, convention)),
2093
+ ...(await inspectOutboxJobDispatcherDrift(targetDir, files, config)),
1778
2094
  ...(await inspectProductionReadiness(targetDir, files, config, convention)),
1779
2095
  ...(await inspectFeatureArtifactDrift(
1780
2096
  targetDir,
@@ -2472,14 +2788,14 @@ async function inspectProductionReadiness(
2472
2788
 
2473
2789
  if (
2474
2790
  containsCallExpression(source, "createDevtoolsRoute") &&
2475
- /\benabled\s*:\s*true\b/.test(source) &&
2476
- !/\bauthorize\s*:/.test(source)
2791
+ !/\bauthorize\s*(?::|\()/.test(source) &&
2792
+ !/\ballowUnauthenticatedDevelopmentAccess\s*:\s*true\b/.test(source)
2477
2793
  ) {
2478
2794
  diagnostics.push({
2479
2795
  severity: "warning",
2480
- code: "BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
2796
+ code: "BEIGNET_DEVTOOLS_ROUTE_POLICY_MISSING",
2481
2797
  file,
2482
- message: `${file} enables devtools with enabled: true and no authorize callback. Keep devtools disabled in production or add app-owned authorization before exposing diagnostic data.`,
2798
+ message: `${file} creates a devtools route without an access policy, so the route remains hidden. Add app-owned authorize(...), or explicitly set allowUnauthenticatedDevelopmentAccess: true only for a development server bound exclusively to loopback.`,
2483
2799
  });
2484
2800
  }
2485
2801
 
@@ -3882,9 +4198,7 @@ async function inspectFeatureArtifactDrift(
3882
4198
  }
3883
4199
 
3884
4200
  for (const file of files) {
3885
- const match = new RegExp(
3886
- `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.test\\.ts$`,
3887
- ).exec(file);
4201
+ const match = directFeatureTestMatch(file, featuresPath);
3888
4202
  if (!match) continue;
3889
4203
  diagnostics.push({
3890
4204
  severity: "warning",
@@ -3896,23 +4210,27 @@ async function inspectFeatureArtifactDrift(
3896
4210
 
3897
4211
  for (const file of files) {
3898
4212
  if (
3899
- !file.endsWith(".ts") ||
3900
- file.endsWith(".test.ts") ||
3901
- file.endsWith(".d.ts")
4213
+ !/\.(?:ts|tsx|mts|cts)$/.test(file) ||
4214
+ /\.d\.(?:ts|mts|cts)$/.test(file)
3902
4215
  ) {
3903
4216
  continue;
3904
4217
  }
3905
4218
 
3906
- const misplaced = misplacedFeatureArtifactFolder(file, featuresPath);
4219
+ const misplaced = directFeatureTestMatch(file, featuresPath)
4220
+ ? undefined
4221
+ : misplacedFeatureLayer(file, config);
3907
4222
  if (misplaced) {
3908
4223
  diagnostics.push({
3909
4224
  severity: "warning",
3910
4225
  code: "BEIGNET_FEATURE_ARTIFACT_FOLDER",
3911
4226
  file,
3912
- message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned workflow artifacts in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
4227
+ message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned files in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
3913
4228
  });
3914
4229
  }
3915
4230
 
4231
+ if (isTestSourceFile(file)) continue;
4232
+ if (!file.endsWith(".ts")) continue;
4233
+
3916
4234
  if (
3917
4235
  isNotificationDefinitionFile(file, featuresPath) ||
3918
4236
  isInfraOrServerFile(file, config)
@@ -3933,6 +4251,15 @@ async function inspectFeatureArtifactDrift(
3933
4251
  return diagnostics;
3934
4252
  }
3935
4253
 
4254
+ function directFeatureTestMatch(
4255
+ file: string,
4256
+ featuresPath: string,
4257
+ ): RegExpExecArray | null {
4258
+ return new RegExp(
4259
+ `^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`,
4260
+ ).exec(file);
4261
+ }
4262
+
3936
4263
  function isFeatureArtifactFile(
3937
4264
  file: string,
3938
4265
  featuresPath: string,
@@ -3965,34 +4292,30 @@ async function appHasUploadRoute(
3965
4292
  return false;
3966
4293
  }
3967
4294
 
3968
- function misplacedFeatureArtifactFolder(
4295
+ function misplacedFeatureLayer(
3969
4296
  file: string,
3970
- featuresPath: string,
4297
+ config: ResolvedBeignetConfig,
3971
4298
  ): { actual: string; expected: string } | undefined {
3972
- const match = new RegExp(
3973
- `^${escapeRegExp(featuresPath)}/([^/]+)/([^/]+)/`,
3974
- ).exec(file);
4299
+ const featuresPath = directoryPath(config.paths.features);
4300
+ const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/(.+)$`).exec(
4301
+ file,
4302
+ );
3975
4303
  if (!match) return undefined;
3976
4304
 
3977
- const folder = match[2];
3978
- const mappings: Record<string, string> = {
3979
- command: "tasks/",
3980
- commands: "tasks/",
3981
- event: "domain/events/",
3982
- events: "domain/events/",
3983
- job: "jobs/",
3984
- listener: "listeners/",
3985
- notification: "notifications/",
3986
- schedule: "schedules/",
3987
- scheduled: "schedules/",
3988
- task: "tasks/",
3989
- upload: "uploads/",
3990
- };
4305
+ const canonicalPath = canonicalFeatureLayerPath(file, config);
4306
+ if (!canonicalPath) return undefined;
3991
4307
 
3992
- const expected = mappings[folder];
3993
- if (!expected) return undefined;
4308
+ const featurePrefix = `${featuresPath}/${match[1]}/`;
4309
+ const actualRelative = file.slice(featurePrefix.length);
4310
+ const expectedRelative = canonicalPath.slice(featurePrefix.length);
4311
+ const actual = actualRelative.includes("/")
4312
+ ? `${actualRelative.split("/")[0]}/`
4313
+ : actualRelative;
4314
+ const expectedDirectory = path.posix.dirname(expectedRelative);
4315
+ const expected =
4316
+ expectedDirectory === "." ? expectedRelative : `${expectedDirectory}/`;
3994
4317
 
3995
- return { actual: `${folder}/`, expected };
4318
+ return { actual, expected };
3996
4319
  }
3997
4320
 
3998
4321
  function isNotificationDefinitionFile(
@@ -4371,10 +4694,7 @@ async function workflowRegistrationDrift(
4371
4694
  // Events without listeners have nothing to deliver on drain, so only
4372
4695
  // record-only events stay out of the registry without a warning.
4373
4696
  const listened = await listenedEventNames(targetDir, files, config);
4374
- const value = outboxRegistryValueInfo(outboxSource, "events");
4375
- const registered = value
4376
- ? identifiersFromArrayExpression(value.text)
4377
- : new Set<string>();
4697
+ const registered = outboxRegistryIdentifiers(outboxSource, "events");
4378
4698
  drift.outbox.events = unregisteredWorkflowRegistries(
4379
4699
  eventRegistries,
4380
4700
  registered,
@@ -4383,10 +4703,7 @@ async function workflowRegistrationDrift(
4383
4703
  }
4384
4704
 
4385
4705
  if (jobRegistries.length > 0) {
4386
- const value = outboxRegistryValueInfo(outboxSource, "jobs");
4387
- const registered = value
4388
- ? identifiersFromArrayExpression(value.text)
4389
- : new Set<string>();
4706
+ const registered = outboxRegistryIdentifiers(outboxSource, "jobs");
4390
4707
  drift.outbox.jobs = unregisteredWorkflowRegistries(
4391
4708
  jobRegistries,
4392
4709
  registered,
@@ -5086,6 +5403,56 @@ async function appHasOutboxDrainEntrypoint(
5086
5403
  return false;
5087
5404
  }
5088
5405
 
5406
+ async function inspectOutboxJobDispatcherDrift(
5407
+ targetDir: string,
5408
+ files: string[],
5409
+ config: ResolvedBeignetConfig,
5410
+ ): Promise<InspectDiagnostic[]> {
5411
+ if (!hasOutboxRegistry(files, config)) return [];
5412
+
5413
+ const source = await readFile(
5414
+ path.join(targetDir, config.paths.outbox),
5415
+ "utf8",
5416
+ );
5417
+ const jobsState = outboxRegistryEntryState(source, "jobs");
5418
+ if (jobsState === "absent" || jobsState === "empty") return [];
5419
+
5420
+ const analysis = await analyzePortWiringFiles({ targetDir, files, config });
5421
+ const declaresJobs = analysis.declared.has("jobs");
5422
+ const wiresJobs = analysis.bound.has("jobs") || analysis.deferred.has("jobs");
5423
+ if (declaresJobs && wiresJobs) return [];
5424
+
5425
+ const file =
5426
+ jobsState === "indeterminate"
5427
+ ? config.paths.outbox
5428
+ : !declaresJobs
5429
+ ? config.paths.ports
5430
+ : config.paths.portWiring;
5431
+ const reason = !analysis.portsFileExists
5432
+ ? `${config.paths.ports} does not exist at the configured path`
5433
+ : !declaresJobs
5434
+ ? analysis.declarationIndeterminate
5435
+ ? `${config.paths.ports} does not expose a statically verifiable jobs declaration`
5436
+ : `${config.paths.ports} does not declare a jobs port`
5437
+ : !analysis.portWiringFileExists
5438
+ ? `${config.paths.portWiring} does not exist at the configured path`
5439
+ : analysis.indeterminate
5440
+ ? `${config.paths.portWiring} does not expose statically verifiable jobs wiring`
5441
+ : `${config.paths.portWiring} does not bind or defer the jobs port`;
5442
+
5443
+ return [
5444
+ {
5445
+ severity: "warning",
5446
+ code: "BEIGNET_OUTBOX_JOB_DISPATCHER_MISSING",
5447
+ file,
5448
+ message:
5449
+ jobsState === "indeterminate"
5450
+ ? `The jobs entries in the outbox registry at ${config.paths.outbox} could not be statically resolved, and ${reason}. Keep the jobs property explicit so doctor can verify it, or add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`
5451
+ : `The outbox registry in ${config.paths.outbox} contains jobs, but ${reason}. Add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`,
5452
+ },
5453
+ ];
5454
+ }
5455
+
5089
5456
  async function inspectPortProviderDrift(
5090
5457
  targetDir: string,
5091
5458
  files: string[],
@@ -8384,8 +8751,9 @@ async function contractsFromRoutesSource(
8384
8751
  source,
8385
8752
  routeIdentifier,
8386
8753
  );
8754
+ const { contracts } = await readContracts(targetDir, files, config);
8387
8755
  const registeredContracts = contractsForRegisteredRouteGroups(
8388
- await readContracts(targetDir, files, config),
8756
+ contracts,
8389
8757
  routeGroups,
8390
8758
  registeredGroups,
8391
8759
  );