@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.
- package/CHANGELOG.md +34 -0
- package/README.md +76 -21
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.js +37 -3
- package/dist/check.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +415 -168
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +11 -4
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +2 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +19 -19
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +142 -7
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +16 -0
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +9 -1
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +3 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +46 -5
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/inspect.ts +583 -215
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make.ts +225 -8
- package/src/mcp.ts +19 -22
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +16 -0
- package/src/templates/base.ts +9 -1
- package/src/templates/server.ts +2 -0
- 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
|
-
|
|
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
|
|
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<
|
|
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
|
-
|
|
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
|
|
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):
|
|
905
|
-
const contracts:
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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(
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
952
|
-
inheritedCatalogErrors: InspectedCatalogErrorRef[] = [],
|
|
1053
|
+
state: ContractMetadataState,
|
|
953
1054
|
): InspectedContractMetadata | undefined {
|
|
954
|
-
const authRequired =
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
const pathMatch = /path\s*:\s*["']([^"']+)["']/.exec(configSource);
|
|
993
|
-
if (!methodMatch || !pathMatch) return undefined;
|
|
1102
|
+
return state;
|
|
1103
|
+
}
|
|
994
1104
|
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1216
|
+
type ContractCall = {
|
|
1217
|
+
name: string;
|
|
1218
|
+
arguments: readonly ts.Expression[];
|
|
1219
|
+
node: ts.CallExpression;
|
|
1220
|
+
};
|
|
1023
1221
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1222
|
+
type ContractCallChain = {
|
|
1223
|
+
baseIdentifier?: string;
|
|
1224
|
+
rootCall?: ContractCall;
|
|
1225
|
+
calls: ContractCall[];
|
|
1226
|
+
};
|
|
1026
1227
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
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
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
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
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
): InspectedCatalogErrorRef[]
|
|
1079
|
-
|
|
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
|
|
1082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
prefixes: Map<string, string>,
|
|
1090
|
-
): string | undefined {
|
|
1091
|
-
if (expression.includes("defineContractGroup(")) return "";
|
|
1386
|
+
return merged;
|
|
1387
|
+
}
|
|
1092
1388
|
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
2476
|
-
!/\
|
|
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: "
|
|
2796
|
+
code: "BEIGNET_DEVTOOLS_ROUTE_POLICY_MISSING",
|
|
2481
2797
|
file,
|
|
2482
|
-
message: `${file}
|
|
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 =
|
|
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
|
-
|
|
3900
|
-
|
|
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 =
|
|
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
|
|
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
|
|
4295
|
+
function misplacedFeatureLayer(
|
|
3969
4296
|
file: string,
|
|
3970
|
-
|
|
4297
|
+
config: ResolvedBeignetConfig,
|
|
3971
4298
|
): { actual: string; expected: string } | undefined {
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
|
|
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
|
|
3978
|
-
|
|
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
|
|
3993
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
8756
|
+
contracts,
|
|
8389
8757
|
routeGroups,
|
|
8390
8758
|
registeredGroups,
|
|
8391
8759
|
);
|