@beignet/cli 0.0.51 → 0.0.53
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 +46 -0
- package/README.md +182 -19
- package/dist/analysis/layers.d.ts +2 -0
- package/dist/analysis/layers.d.ts.map +1 -1
- package/dist/analysis/layers.js +9 -2
- package/dist/analysis/layers.js.map +1 -1
- package/dist/analysis/port-wiring.d.ts +4 -2
- package/dist/analysis/port-wiring.d.ts.map +1 -1
- package/dist/analysis/port-wiring.js +41 -11
- package/dist/analysis/port-wiring.js.map +1 -1
- package/dist/analysis/source-files.d.ts +15 -0
- package/dist/analysis/source-files.d.ts.map +1 -0
- package/dist/analysis/source-files.js +52 -0
- package/dist/analysis/source-files.js.map +1 -0
- package/dist/analysis/source-index.d.ts +2 -1
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +34 -5
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace-routes.d.ts +8 -0
- package/dist/analysis/workspace-routes.d.ts.map +1 -0
- package/dist/analysis/workspace-routes.js +291 -0
- package/dist/analysis/workspace-routes.js.map +1 -0
- package/dist/analysis/workspace.d.ts +24 -3
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +184 -43
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts.map +1 -1
- package/dist/app-map-changes.js +24 -14
- package/dist/app-map-changes.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +87 -81
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +92 -25
- package/dist/check.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/git-changes.d.ts +17 -1
- package/dist/git-changes.d.ts.map +1 -1
- package/dist/git-changes.js +118 -84
- package/dist/git-changes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +424 -129
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +108 -43
- package/dist/lint.js.map +1 -1
- package/dist/make/shared.js +1 -1
- package/dist/make.js +18 -1
- package/dist/make.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.js +2 -2
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +22 -6
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +36 -13
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.js +5 -5
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +51 -22
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +60 -20
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +51 -23
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/shadcn.js +1 -1
- package/dist/templates/shadcn.js.map +1 -1
- package/dist/templates/shared.js +6 -6
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts.map +1 -1
- package/dist/templates/testing.js +7 -19
- package/dist/templates/testing.js.map +1 -1
- package/dist/test-discovery.d.ts +9 -0
- package/dist/test-discovery.d.ts.map +1 -0
- package/dist/test-discovery.js +40 -0
- package/dist/test-discovery.js.map +1 -0
- package/package.json +8 -5
- package/skills/app-structure/SKILL.md +74 -7
- package/src/analysis/layers.ts +13 -2
- package/src/analysis/port-wiring.ts +68 -11
- package/src/analysis/source-files.ts +61 -0
- package/src/analysis/source-index.ts +56 -4
- package/src/analysis/workspace-routes.ts +385 -0
- package/src/analysis/workspace.ts +302 -59
- package/src/app-map-changes.ts +41 -13
- package/src/app-map.ts +114 -103
- package/src/check.ts +115 -25
- package/src/config.ts +33 -0
- package/src/git-changes.ts +218 -86
- package/src/index.ts +33 -5
- package/src/inspect.ts +608 -156
- package/src/lint.ts +152 -48
- package/src/make/shared.ts +1 -1
- package/src/make.ts +31 -1
- package/src/mcp.ts +1 -1
- package/src/provider-add.ts +2 -2
- package/src/provider-audit.ts +30 -10
- package/src/templates/agents.ts +36 -13
- package/src/templates/base.ts +5 -5
- package/src/templates/db/mysql.ts +51 -22
- package/src/templates/db/postgres.ts +60 -20
- package/src/templates/db/sqlite.ts +51 -23
- package/src/templates/shadcn.ts +1 -1
- package/src/templates/shared.ts +6 -6
- package/src/templates/testing.ts +12 -19
- package/src/test-discovery.ts +53 -0
package/src/app-map.ts
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
isImportedContractDeclaration,
|
|
7
7
|
} from "./analysis/contract-factories.js";
|
|
8
8
|
import { analyzePortWiring } from "./analysis/port-wiring.js";
|
|
9
|
+
import {
|
|
10
|
+
isAnalysisSourceFile,
|
|
11
|
+
stripSourceFileExtension,
|
|
12
|
+
} from "./analysis/source-files.js";
|
|
9
13
|
import {
|
|
10
14
|
createSourceIndex,
|
|
11
15
|
exportedVariableDeclarations,
|
|
@@ -19,7 +23,10 @@ import {
|
|
|
19
23
|
import {
|
|
20
24
|
type AnalysisWorkspace,
|
|
21
25
|
createAnalysisWorkspace,
|
|
26
|
+
projectPath,
|
|
27
|
+
projectSource,
|
|
22
28
|
} from "./analysis/workspace.js";
|
|
29
|
+
import { workspaceRouteContracts } from "./analysis/workspace-routes.js";
|
|
23
30
|
import type { AppMapNodeKind } from "./app-map-schema.js";
|
|
24
31
|
import { directoryPath } from "./config.js";
|
|
25
32
|
import {
|
|
@@ -29,6 +36,11 @@ import {
|
|
|
29
36
|
} from "./inspect.js";
|
|
30
37
|
import { type LintDiagnostic, lintApp } from "./lint.js";
|
|
31
38
|
import { auditProviders } from "./provider-audit.js";
|
|
39
|
+
import {
|
|
40
|
+
appTestRoots,
|
|
41
|
+
isAppTestFile,
|
|
42
|
+
isWithinAppTestRoots,
|
|
43
|
+
} from "./test-discovery.js";
|
|
32
44
|
|
|
33
45
|
export type { AppMapNodeKind } from "./app-map-schema.js";
|
|
34
46
|
export { appMapNodeKinds } from "./app-map-schema.js";
|
|
@@ -206,8 +218,8 @@ export async function mapApp(
|
|
|
206
218
|
const definitions = new Map<string, DefinitionRecord>();
|
|
207
219
|
const definitionBySource = new Map<string, string>();
|
|
208
220
|
const registries: RegistryRecord[] = [];
|
|
209
|
-
const
|
|
210
|
-
const featureNames =
|
|
221
|
+
const features = discoverFeatures(workspace);
|
|
222
|
+
const featureNames = [...features.keys()];
|
|
211
223
|
|
|
212
224
|
const addNode = (node: AppMapNode) => {
|
|
213
225
|
const existing = nodes.get(node.id);
|
|
@@ -225,11 +237,9 @@ export async function mapApp(
|
|
|
225
237
|
};
|
|
226
238
|
const addEdge = (edge: AppMapEdge) => edges.push(edge);
|
|
227
239
|
|
|
228
|
-
for (const feature of
|
|
229
|
-
const featureFiles =
|
|
230
|
-
|
|
231
|
-
featuresPath,
|
|
232
|
-
feature,
|
|
240
|
+
for (const [feature, featurePath] of features) {
|
|
241
|
+
const featureFiles = workspace.files.filter((file) =>
|
|
242
|
+
file.startsWith(`${featurePath}/`),
|
|
233
243
|
);
|
|
234
244
|
const roles = summarizeFeatureFiles(featureFiles);
|
|
235
245
|
addNode({
|
|
@@ -237,21 +247,31 @@ export async function mapApp(
|
|
|
237
247
|
kind: "feature",
|
|
238
248
|
name: feature,
|
|
239
249
|
feature,
|
|
240
|
-
source: { file:
|
|
250
|
+
source: { file: featurePath },
|
|
241
251
|
status: "declared",
|
|
242
252
|
details: roles,
|
|
243
253
|
});
|
|
254
|
+
}
|
|
244
255
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
for (const file of workspace.files.filter(
|
|
257
|
+
(candidate) =>
|
|
258
|
+
isAppTestFile(candidate) &&
|
|
259
|
+
isWithinAppTestRoots(
|
|
260
|
+
projectSource(workspace, candidate).file,
|
|
261
|
+
appTestRoots(projectSource(workspace, candidate).project.config.paths),
|
|
262
|
+
),
|
|
263
|
+
)) {
|
|
264
|
+
const id = `test:${file}`;
|
|
265
|
+
const feature = featureForFile(file, workspace);
|
|
266
|
+
addNode({
|
|
267
|
+
id,
|
|
268
|
+
kind: "test",
|
|
269
|
+
name: path.posix.basename(file),
|
|
270
|
+
...(feature ? { feature } : {}),
|
|
271
|
+
source: { file },
|
|
272
|
+
status: "declared",
|
|
273
|
+
});
|
|
274
|
+
if (feature) {
|
|
255
275
|
addEdge(exactEdge("contains-test", featureId(feature), id, file));
|
|
256
276
|
}
|
|
257
277
|
}
|
|
@@ -260,7 +280,7 @@ export async function mapApp(
|
|
|
260
280
|
const contractIdsByExport = new Map<string, string[]>();
|
|
261
281
|
for (const contract of inspection.contracts) {
|
|
262
282
|
const id = `contract:${contract.file}#${contract.exportName}`;
|
|
263
|
-
const feature = featureForFile(contract.file,
|
|
283
|
+
const feature = featureForFile(contract.file, workspace);
|
|
264
284
|
contractIds.set(symbolKey(contract.file, contract.exportName), id);
|
|
265
285
|
const exports = contractIdsByExport.get(contract.exportName) ?? [];
|
|
266
286
|
exports.push(id);
|
|
@@ -305,8 +325,8 @@ export async function mapApp(
|
|
|
305
325
|
id,
|
|
306
326
|
kind: "route",
|
|
307
327
|
name: `${route.method} ${route.path}`,
|
|
308
|
-
...(featureForFile(route.file,
|
|
309
|
-
? { feature: featureForFile(route.file,
|
|
328
|
+
...(featureForFile(route.file, workspace)
|
|
329
|
+
? { feature: featureForFile(route.file, workspace) }
|
|
310
330
|
: {}),
|
|
311
331
|
source: {
|
|
312
332
|
file: route.handlerFile ?? route.file,
|
|
@@ -345,7 +365,7 @@ export async function mapApp(
|
|
|
345
365
|
details: { type: "server" },
|
|
346
366
|
});
|
|
347
367
|
}
|
|
348
|
-
const sourceFiles = workspace.files.filter(
|
|
368
|
+
const sourceFiles = workspace.files.filter(isAnalysisSourceFile);
|
|
349
369
|
for (const file of sourceFiles) {
|
|
350
370
|
const sourceFile = await workspace.readSourceFile(file);
|
|
351
371
|
for (const { exportName, declaration } of exportedVariableDeclarations(
|
|
@@ -359,7 +379,7 @@ export async function mapApp(
|
|
|
359
379
|
const runtimeName = literalText(useCaseCall.arguments[0]);
|
|
360
380
|
if (runtimeName) {
|
|
361
381
|
const kind = callMemberName(useCaseCall) as "command" | "query";
|
|
362
|
-
const feature = featureForFile(file,
|
|
382
|
+
const feature = featureForFile(file, workspace);
|
|
363
383
|
const id = `use-case:${runtimeName}`;
|
|
364
384
|
const node: AppMapNode = {
|
|
365
385
|
id,
|
|
@@ -402,7 +422,7 @@ export async function mapApp(
|
|
|
402
422
|
: undefined;
|
|
403
423
|
const runtimeName = definitionRuntimeName(call, definitionCall);
|
|
404
424
|
if (kind && runtimeName) {
|
|
405
|
-
const feature = featureForFile(file,
|
|
425
|
+
const feature = featureForFile(file, workspace);
|
|
406
426
|
const id = `${kind}:${runtimeName}`;
|
|
407
427
|
const status = definitionStatus(
|
|
408
428
|
kind,
|
|
@@ -442,7 +462,7 @@ export async function mapApp(
|
|
|
442
462
|
|
|
443
463
|
const policyCall = findCall(initializer, new Set(["definePolicy"]));
|
|
444
464
|
if (policyCall && ts.isObjectLiteralExpression(policyCall.arguments[0])) {
|
|
445
|
-
const feature = featureForFile(file,
|
|
465
|
+
const feature = featureForFile(file, workspace);
|
|
446
466
|
const policyId = `policy:${file}#${exportName}`;
|
|
447
467
|
addNode({
|
|
448
468
|
id: policyId,
|
|
@@ -660,8 +680,11 @@ export async function mapApp(
|
|
|
660
680
|
contractIdsByExport,
|
|
661
681
|
routeIds,
|
|
662
682
|
definitionBySource,
|
|
663
|
-
featuresPath,
|
|
664
683
|
diagnostics: inspection.diagnostics,
|
|
684
|
+
registeredGroups:
|
|
685
|
+
workspace.projects.length > 1
|
|
686
|
+
? (await workspaceRouteContracts(workspace)).groups
|
|
687
|
+
: undefined,
|
|
665
688
|
});
|
|
666
689
|
|
|
667
690
|
await addOpenApiSurface({
|
|
@@ -695,7 +718,6 @@ export async function mapApp(
|
|
|
695
718
|
await addUnresolvedLocalImports({ workspace, unresolved });
|
|
696
719
|
await addFeatureDependencies({
|
|
697
720
|
workspace,
|
|
698
|
-
featuresPath,
|
|
699
721
|
addEdge,
|
|
700
722
|
featureNames: new Set(featureNames),
|
|
701
723
|
});
|
|
@@ -912,13 +934,14 @@ async function addRouteGroups(args: {
|
|
|
912
934
|
contractIdsByExport: Map<string, string[]>;
|
|
913
935
|
routeIds: Map<string, string>;
|
|
914
936
|
definitionBySource: Map<string, string>;
|
|
915
|
-
|
|
937
|
+
registeredGroups?: Set<string>;
|
|
916
938
|
diagnostics: InspectDiagnostic[];
|
|
917
939
|
}): Promise<void> {
|
|
918
940
|
const routeFiles = args.workspace.files.filter(
|
|
919
941
|
(file) =>
|
|
920
|
-
featureForFile(file, args.
|
|
921
|
-
file
|
|
942
|
+
featureForFile(file, args.workspace) !== undefined &&
|
|
943
|
+
isAnalysisSourceFile(file) &&
|
|
944
|
+
stripSourceFileExtension(file).endsWith("/routes"),
|
|
922
945
|
);
|
|
923
946
|
|
|
924
947
|
for (const file of routeFiles) {
|
|
@@ -932,17 +955,20 @@ async function addRouteGroups(args: {
|
|
|
932
955
|
const options = call?.arguments[0];
|
|
933
956
|
if (!call || !options || !ts.isObjectLiteralExpression(options)) continue;
|
|
934
957
|
|
|
935
|
-
const feature = featureForFile(file, args.
|
|
958
|
+
const feature = featureForFile(file, args.workspace);
|
|
936
959
|
const groupName = literalText(
|
|
937
960
|
propertyAssignment(options, "name")?.initializer,
|
|
938
961
|
);
|
|
939
962
|
const id = `route-group:${file}#${exportName}`;
|
|
940
|
-
const missing =
|
|
941
|
-
(
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
diagnostic
|
|
945
|
-
|
|
963
|
+
const missing =
|
|
964
|
+
(args.registeredGroups !== undefined &&
|
|
965
|
+
!args.registeredGroups.has(`${file}#${exportName}`)) ||
|
|
966
|
+
args.diagnostics.some(
|
|
967
|
+
(diagnostic) =>
|
|
968
|
+
diagnostic.code === "BEIGNET_ROUTE_GROUP_UNREGISTERED" &&
|
|
969
|
+
diagnostic.subject?.file === file &&
|
|
970
|
+
diagnostic.subject.exportName === exportName,
|
|
971
|
+
);
|
|
946
972
|
args.addNode({
|
|
947
973
|
id,
|
|
948
974
|
kind: "route-group",
|
|
@@ -1063,8 +1089,10 @@ async function addPorts(args: {
|
|
|
1063
1089
|
}): Promise<void> {
|
|
1064
1090
|
const portsFile = args.workspace.config.paths.ports;
|
|
1065
1091
|
if (!args.workspace.fileSet.has(portsFile)) return;
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1092
|
+
const analysis = await analyzePortWiring(
|
|
1093
|
+
args.workspace,
|
|
1094
|
+
args.workspace.projects.length > 1 ? args.sourceIndex : undefined,
|
|
1095
|
+
);
|
|
1068
1096
|
if (analysis.declarationIndeterminate) {
|
|
1069
1097
|
args.unresolved.push({
|
|
1070
1098
|
code: "app_ports_declaration_unresolved",
|
|
@@ -1081,11 +1109,12 @@ async function addPorts(args: {
|
|
|
1081
1109
|
}
|
|
1082
1110
|
for (const name of analysis.declared) {
|
|
1083
1111
|
const property = analysis.properties.get(name);
|
|
1112
|
+
const propertyFile = analysis.propertyFiles.get(name) ?? portsFile;
|
|
1084
1113
|
const feature = property
|
|
1085
1114
|
? await portFeature(
|
|
1086
1115
|
property.type,
|
|
1087
|
-
|
|
1088
|
-
args.workspace
|
|
1116
|
+
propertyFile,
|
|
1117
|
+
args.workspace,
|
|
1089
1118
|
args.sourceIndex,
|
|
1090
1119
|
)
|
|
1091
1120
|
: undefined;
|
|
@@ -1096,7 +1125,7 @@ async function addPorts(args: {
|
|
|
1096
1125
|
name,
|
|
1097
1126
|
...(feature ? { feature } : {}),
|
|
1098
1127
|
source: property
|
|
1099
|
-
? await args.sourceIndex.sourceLocation(
|
|
1128
|
+
? await args.sourceIndex.sourceLocation(propertyFile, property)
|
|
1100
1129
|
: { file: args.workspace.config.paths.portWiring },
|
|
1101
1130
|
status:
|
|
1102
1131
|
analysis.bound.has(name) || analysis.deferred.has(name)
|
|
@@ -1108,7 +1137,7 @@ async function addPorts(args: {
|
|
|
1108
1137
|
: analysis.deferred.has(name)
|
|
1109
1138
|
? "deferred"
|
|
1110
1139
|
: "unresolved",
|
|
1111
|
-
...(property?.type ? { type: property.type.getText(
|
|
1140
|
+
...(property?.type ? { type: property.type.getText() } : {}),
|
|
1112
1141
|
},
|
|
1113
1142
|
});
|
|
1114
1143
|
if (feature) {
|
|
@@ -1192,8 +1221,8 @@ async function addOutboxDeliveryGraph(args: {
|
|
|
1192
1221
|
]);
|
|
1193
1222
|
for (const file of args.workspace.files) {
|
|
1194
1223
|
if (
|
|
1195
|
-
!
|
|
1196
|
-
|
|
1224
|
+
!isAnalysisSourceFile(file) ||
|
|
1225
|
+
isAppTestFile(file) ||
|
|
1197
1226
|
![...runtimeDirectories].some(
|
|
1198
1227
|
(directory) =>
|
|
1199
1228
|
directory === "." ||
|
|
@@ -1276,9 +1305,10 @@ function isProviderLifecycleSource(
|
|
|
1276
1305
|
file: string,
|
|
1277
1306
|
sourceFile: ts.SourceFile,
|
|
1278
1307
|
): boolean {
|
|
1308
|
+
const sourcePath = stripSourceFileExtension(file);
|
|
1279
1309
|
if (
|
|
1280
|
-
/(^|\/)provider(s)
|
|
1281
|
-
/(^|\/)[^/]*provider[^/]
|
|
1310
|
+
/(^|\/)provider(s)?$/.test(sourcePath) ||
|
|
1311
|
+
/(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
|
|
1282
1312
|
/(^|\/)providers?\//.test(file)
|
|
1283
1313
|
) {
|
|
1284
1314
|
return true;
|
|
@@ -1390,7 +1420,7 @@ async function addTables(args: {
|
|
|
1390
1420
|
const schemaPrefix = `${infraDir}/db/schema/`;
|
|
1391
1421
|
const files = args.workspace.files.filter(
|
|
1392
1422
|
(file) =>
|
|
1393
|
-
|
|
1423
|
+
isAnalysisSourceFile(file) &&
|
|
1394
1424
|
(file.startsWith(schemaPrefix) ||
|
|
1395
1425
|
schemaSources.some((source) =>
|
|
1396
1426
|
file.startsWith(staticPathPrefix(source)),
|
|
@@ -1429,7 +1459,6 @@ async function addTables(args: {
|
|
|
1429
1459
|
|
|
1430
1460
|
async function addFeatureDependencies(args: {
|
|
1431
1461
|
workspace: AnalysisWorkspace;
|
|
1432
|
-
featuresPath: string;
|
|
1433
1462
|
addEdge(edge: AppMapEdge): void;
|
|
1434
1463
|
featureNames: Set<string>;
|
|
1435
1464
|
}): Promise<void> {
|
|
@@ -1447,12 +1476,12 @@ async function addFeatureDependencies(args: {
|
|
|
1447
1476
|
}
|
|
1448
1477
|
>();
|
|
1449
1478
|
|
|
1450
|
-
for (const file of args.workspace.files.filter(
|
|
1451
|
-
const from = featureForFile(file, args.
|
|
1479
|
+
for (const file of args.workspace.files.filter(isAnalysisSourceFile)) {
|
|
1480
|
+
const from = featureForFile(file, args.workspace);
|
|
1452
1481
|
if (!from || !args.featureNames.has(from)) continue;
|
|
1453
1482
|
for (const reference of await localImportReferences(args.workspace, file)) {
|
|
1454
1483
|
if (!reference.resolvedFile) continue;
|
|
1455
|
-
const to = featureForFile(reference.resolvedFile, args.
|
|
1484
|
+
const to = featureForFile(reference.resolvedFile, args.workspace);
|
|
1456
1485
|
if (!to || to === from || !args.featureNames.has(to)) continue;
|
|
1457
1486
|
const key = `${from}->${to}`;
|
|
1458
1487
|
const current = dependencies.get(key) ?? {
|
|
@@ -1496,7 +1525,7 @@ async function addUnresolvedLocalImports(args: {
|
|
|
1496
1525
|
workspace: AnalysisWorkspace;
|
|
1497
1526
|
unresolved: AppMapUnresolvedReference[];
|
|
1498
1527
|
}): Promise<void> {
|
|
1499
|
-
for (const file of args.workspace.files.filter(
|
|
1528
|
+
for (const file of args.workspace.files.filter(isAnalysisSourceFile)) {
|
|
1500
1529
|
for (const reference of await localImportReferences(args.workspace, file)) {
|
|
1501
1530
|
if (reference.resolvedFile) continue;
|
|
1502
1531
|
args.unresolved.push({
|
|
@@ -1551,20 +1580,21 @@ async function addUnresolvedContractDeclarations(args: {
|
|
|
1551
1580
|
}
|
|
1552
1581
|
|
|
1553
1582
|
function appMapContractSourceFiles(workspace: AnalysisWorkspace): string[] {
|
|
1554
|
-
const contractsPath = directoryPath(workspace.config.paths.contracts);
|
|
1555
|
-
const featureOwned =
|
|
1556
|
-
contractsPath === directoryPath(workspace.config.paths.features);
|
|
1557
|
-
|
|
1558
1583
|
return workspace.files.filter((file) => {
|
|
1559
|
-
if (!file
|
|
1584
|
+
if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
|
|
1585
|
+
const source = projectSource(workspace, file);
|
|
1586
|
+
const contractsPath = directoryPath(source.project.config.paths.contracts);
|
|
1587
|
+
const featureOwned =
|
|
1588
|
+
contractsPath === directoryPath(source.project.config.paths.features);
|
|
1589
|
+
const sourcePath = stripSourceFileExtension(source.file);
|
|
1560
1590
|
if (featureOwned) {
|
|
1561
1591
|
const prefix = `${contractsPath}/`;
|
|
1562
|
-
if (!file.startsWith(prefix)) return false;
|
|
1563
|
-
const parts =
|
|
1564
|
-
return parts.length === 2 && parts[1] === "contracts
|
|
1592
|
+
if (!source.file.startsWith(prefix)) return false;
|
|
1593
|
+
const parts = sourcePath.slice(prefix.length).split("/");
|
|
1594
|
+
return parts.length === 2 && parts[1] === "contracts";
|
|
1565
1595
|
}
|
|
1566
1596
|
return (
|
|
1567
|
-
|
|
1597
|
+
sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`)
|
|
1568
1598
|
);
|
|
1569
1599
|
});
|
|
1570
1600
|
}
|
|
@@ -1580,10 +1610,7 @@ async function registryRecord(args: {
|
|
|
1580
1610
|
if (!initializer) return undefined;
|
|
1581
1611
|
const registry = registryValues(initializer, args.exportName);
|
|
1582
1612
|
if (!registry) return undefined;
|
|
1583
|
-
const feature = featureForFile(
|
|
1584
|
-
args.file,
|
|
1585
|
-
directoryPath(args.workspace.config.paths.features),
|
|
1586
|
-
);
|
|
1613
|
+
const feature = featureForFile(args.file, args.workspace);
|
|
1587
1614
|
return {
|
|
1588
1615
|
file: args.file,
|
|
1589
1616
|
declaration: args.declaration,
|
|
@@ -1795,27 +1822,19 @@ function arrayExpressions(array: ts.ArrayLiteralExpression): ts.Expression[] {
|
|
|
1795
1822
|
});
|
|
1796
1823
|
}
|
|
1797
1824
|
|
|
1798
|
-
function
|
|
1799
|
-
|
|
1800
|
-
featuresPath: string,
|
|
1801
|
-
): string[] {
|
|
1802
|
-
const names = new Set<string>();
|
|
1803
|
-
const prefix = `${featuresPath}/`;
|
|
1825
|
+
function discoverFeatures(workspace: AnalysisWorkspace): Map<string, string> {
|
|
1826
|
+
const features = new Map<string, string>();
|
|
1804
1827
|
for (const file of workspace.files) {
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1828
|
+
const feature = featureForFile(file, workspace);
|
|
1829
|
+
if (!feature) continue;
|
|
1830
|
+
const source = projectSource(workspace, file);
|
|
1831
|
+
const root = directoryPath(source.project.config.paths.features);
|
|
1832
|
+
const name = source.file.slice(`${root}/`.length).split("/")[0];
|
|
1833
|
+
features.set(feature, projectPath(source.project, `${root}/${name}`));
|
|
1808
1834
|
}
|
|
1809
|
-
return
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
function filesForFeature(
|
|
1813
|
-
files: string[],
|
|
1814
|
-
featuresPath: string,
|
|
1815
|
-
feature: string,
|
|
1816
|
-
): string[] {
|
|
1817
|
-
const prefix = `${featuresPath}/${feature}/`;
|
|
1818
|
-
return files.filter((file) => file.startsWith(prefix));
|
|
1835
|
+
return new Map(
|
|
1836
|
+
[...features].sort(([left], [right]) => left.localeCompare(right)),
|
|
1837
|
+
);
|
|
1819
1838
|
}
|
|
1820
1839
|
|
|
1821
1840
|
function summarizeFeatureFiles(files: string[]): Record<string, number> {
|
|
@@ -1824,24 +1843,26 @@ function summarizeFeatureFiles(files: string[]): Record<string, number> {
|
|
|
1824
1843
|
client: files.filter((file) => file.includes("/client/")).length,
|
|
1825
1844
|
components: files.filter((file) => file.includes("/components/")).length,
|
|
1826
1845
|
domain: files.filter((file) => file.includes("/domain/")).length,
|
|
1827
|
-
tests: files.filter(
|
|
1846
|
+
tests: files.filter(isAppTestFile).length,
|
|
1828
1847
|
};
|
|
1829
1848
|
}
|
|
1830
1849
|
|
|
1831
1850
|
function featureForFile(
|
|
1832
1851
|
file: string,
|
|
1833
|
-
|
|
1852
|
+
workspace: AnalysisWorkspace,
|
|
1834
1853
|
): string | undefined {
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1854
|
+
const source = projectSource(workspace, file);
|
|
1855
|
+
const prefix = `${directoryPath(source.project.config.paths.features)}/`;
|
|
1856
|
+
if (!source.file.startsWith(prefix)) return undefined;
|
|
1857
|
+
const feature = source.file.slice(prefix.length).split("/")[0];
|
|
1858
|
+
if (!feature || feature === "shared") return undefined;
|
|
1859
|
+
return source.project.prefix ? `${source.project.name}:${feature}` : feature;
|
|
1839
1860
|
}
|
|
1840
1861
|
|
|
1841
1862
|
async function portFeature(
|
|
1842
1863
|
type: ts.TypeNode | undefined,
|
|
1843
1864
|
file: string,
|
|
1844
|
-
|
|
1865
|
+
workspace: AnalysisWorkspace,
|
|
1845
1866
|
sourceIndex: ReturnType<typeof createSourceIndex>,
|
|
1846
1867
|
): Promise<string | undefined> {
|
|
1847
1868
|
if (
|
|
@@ -1852,9 +1873,7 @@ async function portFeature(
|
|
|
1852
1873
|
return undefined;
|
|
1853
1874
|
}
|
|
1854
1875
|
const target = await sourceIndex.resolveName(file, type.typeName.text);
|
|
1855
|
-
return target
|
|
1856
|
-
? featureForFile(target.file, directoryPath(featuresPath))
|
|
1857
|
-
: undefined;
|
|
1876
|
+
return target ? featureForFile(target.file, workspace) : undefined;
|
|
1858
1877
|
}
|
|
1859
1878
|
|
|
1860
1879
|
function definitionStatus(
|
|
@@ -2017,14 +2036,6 @@ function sourceNodeEdge(
|
|
|
2017
2036
|
};
|
|
2018
2037
|
}
|
|
2019
2038
|
|
|
2020
|
-
function isTestFile(file: string): boolean {
|
|
2021
|
-
return /\.(test|spec)\.(ts|tsx|mts|cts)$/.test(file);
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
function isTypeScriptSource(file: string): boolean {
|
|
2025
|
-
return /\.(ts|tsx|mts|cts)$/.test(file) && !file.endsWith(".d.ts");
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
2039
|
function staticPathPrefix(pattern: string): string {
|
|
2029
2040
|
const wildcard = pattern.search(/[?*[{]/);
|
|
2030
2041
|
return wildcard === -1 ? pattern : pattern.slice(0, wildcard);
|
package/src/check.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createPainter } from "./ansi.js";
|
|
4
4
|
import { detectPackageManager, spawnCommand } from "./db.js";
|
|
5
|
+
import {
|
|
6
|
+
type GitSourceSnapshot,
|
|
7
|
+
readGitSourceSnapshot,
|
|
8
|
+
} from "./git-changes.js";
|
|
5
9
|
import {
|
|
6
10
|
applyDoctorFixes,
|
|
7
11
|
formatDoctor,
|
|
@@ -33,6 +37,14 @@ export type CheckAppResult = {
|
|
|
33
37
|
ok: boolean;
|
|
34
38
|
fixes: InspectFix[];
|
|
35
39
|
steps: CheckStep[];
|
|
40
|
+
provenance: CheckProvenance;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Source-state evidence captured around one complete check run. */
|
|
44
|
+
export type CheckProvenance = {
|
|
45
|
+
status: "current" | "stale" | "unavailable";
|
|
46
|
+
start: GitSourceSnapshot;
|
|
47
|
+
end: GitSourceSnapshot;
|
|
36
48
|
};
|
|
37
49
|
|
|
38
50
|
export type CheckAppOptions = {
|
|
@@ -74,19 +86,34 @@ const defaultMaxOutputBytes = 64 * 1024;
|
|
|
74
86
|
export async function checkApp(
|
|
75
87
|
options: CheckAppOptions = {},
|
|
76
88
|
): Promise<CheckAppResult> {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
89
|
+
options.signal?.throwIfAborted();
|
|
90
|
+
const targetDir = await resolveCheckTargetDir(options.cwd ?? process.cwd());
|
|
91
|
+
options.signal?.throwIfAborted();
|
|
92
|
+
// Validate the app root before fixes may write; reload execution metadata
|
|
93
|
+
// after the provenance baseline so the selected scripts are covered by it.
|
|
94
|
+
await readAppPackageScripts(targetDir);
|
|
95
|
+
options.signal?.throwIfAborted();
|
|
96
|
+
const fixes = options.fix
|
|
97
|
+
? await applyDoctorFixes({ cwd: targetDir, strict: true })
|
|
98
|
+
: [];
|
|
99
|
+
options.signal?.throwIfAborted();
|
|
80
100
|
const steps: CheckStep[] = [];
|
|
81
101
|
const record = (step: CheckStep): void => {
|
|
82
102
|
steps.push(step);
|
|
83
103
|
options.onStep?.(step);
|
|
84
104
|
};
|
|
85
105
|
|
|
106
|
+
const start = await readGitSourceSnapshot({
|
|
107
|
+
cwd: targetDir,
|
|
108
|
+
signal: options.signal,
|
|
109
|
+
});
|
|
110
|
+
options.signal?.throwIfAborted();
|
|
111
|
+
const scripts = await readAppPackageScripts(targetDir);
|
|
112
|
+
const runner = await detectPackageManager(targetDir);
|
|
86
113
|
options.signal?.throwIfAborted();
|
|
87
114
|
record(await lintStep(targetDir, options));
|
|
88
115
|
options.signal?.throwIfAborted();
|
|
89
|
-
const
|
|
116
|
+
const doctorStep = await runDoctorStep(targetDir, options, fixes);
|
|
90
117
|
record(doctorStep);
|
|
91
118
|
|
|
92
119
|
if (options.preflight || options.preflightConnect) {
|
|
@@ -105,16 +132,38 @@ export async function checkApp(
|
|
|
105
132
|
record(await scriptStep(targetDir, runner, script, scripts, options));
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
options.signal?.throwIfAborted();
|
|
136
|
+
const end = await readGitSourceSnapshot({
|
|
137
|
+
cwd: targetDir,
|
|
138
|
+
signal: options.signal,
|
|
139
|
+
});
|
|
140
|
+
options.signal?.throwIfAborted();
|
|
141
|
+
const provenance = checkProvenance(start, end);
|
|
142
|
+
|
|
108
143
|
return {
|
|
109
144
|
schemaVersion: 1,
|
|
110
145
|
targetDir,
|
|
111
146
|
runner,
|
|
112
|
-
ok:
|
|
147
|
+
ok:
|
|
148
|
+
steps.every((step) => step.status !== "failed") &&
|
|
149
|
+
provenance.status !== "stale",
|
|
113
150
|
fixes,
|
|
114
151
|
steps,
|
|
152
|
+
provenance,
|
|
115
153
|
};
|
|
116
154
|
}
|
|
117
155
|
|
|
156
|
+
async function resolveCheckTargetDir(cwd: string): Promise<string> {
|
|
157
|
+
const targetDir = path.resolve(cwd);
|
|
158
|
+
try {
|
|
159
|
+
return await realpath(targetDir);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
162
|
+
if (code === "ENOENT" || code === "ENOTDIR") return targetDir;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
118
167
|
/**
|
|
119
168
|
* Format a completed step as a single status line, with the failing step's
|
|
120
169
|
* output indented underneath.
|
|
@@ -161,9 +210,35 @@ export function formatCheckSummary(
|
|
|
161
210
|
const parts = [`${counts.passed} passed`];
|
|
162
211
|
if (counts.failed > 0) parts.push(paint(`${counts.failed} failed`, "red"));
|
|
163
212
|
if (counts.skipped > 0) parts.push(`${counts.skipped} skipped`);
|
|
213
|
+
if (result.provenance.status === "stale") {
|
|
214
|
+
parts.push(paint("source state stale", "red"));
|
|
215
|
+
}
|
|
164
216
|
return parts.join(", ");
|
|
165
217
|
}
|
|
166
218
|
|
|
219
|
+
/** Format the source state captured before and after validation. */
|
|
220
|
+
export function formatCheckProvenance(
|
|
221
|
+
result: CheckAppResult,
|
|
222
|
+
options: { color?: boolean } = {},
|
|
223
|
+
): string {
|
|
224
|
+
const paint = createPainter(options.color);
|
|
225
|
+
const { provenance } = result;
|
|
226
|
+
if (provenance.status === "unavailable") {
|
|
227
|
+
return `${paint("-", "cyan")} Source provenance unavailable (not a Git worktree)`;
|
|
228
|
+
}
|
|
229
|
+
if (provenance.status === "stale") {
|
|
230
|
+
return `${paint("✗", "red")} Source state changed while checks were running; rerun beignet check`;
|
|
231
|
+
}
|
|
232
|
+
const snapshot = provenance.end;
|
|
233
|
+
if (snapshot.kind !== "git") {
|
|
234
|
+
throw new Error("Current check provenance must contain a Git snapshot.");
|
|
235
|
+
}
|
|
236
|
+
const head = snapshot.comparison.head
|
|
237
|
+
? `HEAD ${snapshot.comparison.head.slice(0, 8)}`
|
|
238
|
+
: "unborn Git repository";
|
|
239
|
+
return `${paint("✓", "green")} Source state current (${head}, ${shortHash(snapshot.repositoryChangeHash)})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
167
242
|
/**
|
|
168
243
|
* Format a full result at once (JSON-free callers and tests).
|
|
169
244
|
*/
|
|
@@ -172,6 +247,7 @@ export function formatCheck(
|
|
|
172
247
|
options: { color?: boolean } = {},
|
|
173
248
|
): string {
|
|
174
249
|
const lines = result.steps.map((step) => formatCheckStep(step, options));
|
|
250
|
+
lines.push(formatCheckProvenance(result, options));
|
|
175
251
|
return `${lines.join("\n")}\n\n${formatCheckSummary(result, options)}`;
|
|
176
252
|
}
|
|
177
253
|
|
|
@@ -195,11 +271,9 @@ async function lintStep(
|
|
|
195
271
|
async function runDoctorStep(
|
|
196
272
|
targetDir: string,
|
|
197
273
|
options: CheckAppOptions,
|
|
198
|
-
|
|
274
|
+
fixes: InspectFix[],
|
|
275
|
+
): Promise<CheckStep> {
|
|
199
276
|
const startedAt = Date.now();
|
|
200
|
-
const fixes = options.fix
|
|
201
|
-
? await applyDoctorFixes({ cwd: targetDir, strict: true })
|
|
202
|
-
: [];
|
|
203
277
|
const result = await inspectApp({ cwd: targetDir, strict: true });
|
|
204
278
|
result.fixes = fixes;
|
|
205
279
|
const failed = result.diagnostics.some(
|
|
@@ -208,24 +282,40 @@ async function runDoctorStep(
|
|
|
208
282
|
);
|
|
209
283
|
|
|
210
284
|
return {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
durationMs: Date.now() - startedAt,
|
|
224
|
-
},
|
|
225
|
-
fixes,
|
|
285
|
+
name: "beignet doctor --strict",
|
|
286
|
+
command: "beignet doctor --strict",
|
|
287
|
+
status: failed ? "failed" : "passed",
|
|
288
|
+
...(fixes.length > 0
|
|
289
|
+
? {
|
|
290
|
+
detail: `${fixes.length} ${fixes.length === 1 ? "fix" : "fixes"} applied`,
|
|
291
|
+
}
|
|
292
|
+
: {}),
|
|
293
|
+
...(failed
|
|
294
|
+
? { output: formatDoctor(result, { color: options.color }) }
|
|
295
|
+
: {}),
|
|
296
|
+
durationMs: Date.now() - startedAt,
|
|
226
297
|
};
|
|
227
298
|
}
|
|
228
299
|
|
|
300
|
+
function checkProvenance(
|
|
301
|
+
start: GitSourceSnapshot,
|
|
302
|
+
end: GitSourceSnapshot,
|
|
303
|
+
): CheckProvenance {
|
|
304
|
+
if (start.kind === "unavailable" && end.kind === "unavailable") {
|
|
305
|
+
return { status: "unavailable", start, end };
|
|
306
|
+
}
|
|
307
|
+
const current =
|
|
308
|
+
start.kind === "git" &&
|
|
309
|
+
end.kind === "git" &&
|
|
310
|
+
start.repositoryRoot === end.repositoryRoot &&
|
|
311
|
+
start.repositoryChangeHash === end.repositoryChangeHash;
|
|
312
|
+
return { status: current ? "current" : "stale", start, end };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function shortHash(hash: string): string {
|
|
316
|
+
return `${hash.slice(0, 15)}…`;
|
|
317
|
+
}
|
|
318
|
+
|
|
229
319
|
async function scriptStep(
|
|
230
320
|
targetDir: string,
|
|
231
321
|
runner: string,
|