@akanjs/devkit 2.3.9-rc.3 → 2.3.9-rc.5
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/akanContext.ts +8 -2
- package/devkitUtils.test.ts +2 -2
- package/executors.ts +17 -13
- package/getModelFileData.ts +5 -2
- package/package.json +2 -2
- package/workflow/artifacts.ts +415 -0
- package/workflow/executor.ts +266 -0
- package/workflow/index.ts +9 -1399
- package/workflow/plan.ts +375 -0
- package/workflow/primitive.ts +59 -0
- package/workflow/render.ts +252 -0
- package/workflow/source.ts +426 -0
- package/workflow/types.ts +296 -0
- package/workflow/uiPolicy.ts +45 -0
- package/workflow/utils.ts +23 -0
package/akanContext.ts
CHANGED
|
@@ -713,8 +713,14 @@ export class AkanContextAnalyzer {
|
|
|
713
713
|
};
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
-
static findModules(
|
|
717
|
-
|
|
716
|
+
static findModules(
|
|
717
|
+
context: AkanWorkspaceContext,
|
|
718
|
+
moduleName?: string | null,
|
|
719
|
+
{ app = null }: { app?: string | null } = {},
|
|
720
|
+
) {
|
|
721
|
+
const modules = [...context.apps, ...context.libs]
|
|
722
|
+
.filter((sys) => !app || sys.name === app)
|
|
723
|
+
.flatMap((sys) => sys.modules);
|
|
718
724
|
return moduleName
|
|
719
725
|
? modules.filter((module) => module.name === moduleName || module.folderName === moduleName)
|
|
720
726
|
: modules;
|
package/devkitUtils.test.ts
CHANGED
|
@@ -269,11 +269,11 @@ describe("getModelFileData", () => {
|
|
|
269
269
|
].join("\n"),
|
|
270
270
|
);
|
|
271
271
|
await write(
|
|
272
|
-
path.join(root, "apps/demo/lib/post/
|
|
272
|
+
path.join(root, "apps/demo/lib/post/Post.Unit.tsx"),
|
|
273
273
|
"export default function Unit() { return null; }\n",
|
|
274
274
|
);
|
|
275
275
|
await write(
|
|
276
|
-
path.join(root, "apps/demo/lib/post/
|
|
276
|
+
path.join(root, "apps/demo/lib/post/Post.View.tsx"),
|
|
277
277
|
"export default function View() { return null; }\n",
|
|
278
278
|
);
|
|
279
279
|
|
package/executors.ts
CHANGED
|
@@ -626,14 +626,14 @@ export class Executor {
|
|
|
626
626
|
overwrite?: boolean;
|
|
627
627
|
},
|
|
628
628
|
dict: { [key: string]: string } = {},
|
|
629
|
-
options: { [key: string]:
|
|
629
|
+
options: { [key: string]: unknown } = {},
|
|
630
630
|
): Promise<FileContent | null> {
|
|
631
631
|
if (targetPath.endsWith(".ts") || targetPath.endsWith(".tsx")) {
|
|
632
632
|
const getContent = (await import(templatePath)) as {
|
|
633
633
|
default: (
|
|
634
634
|
scanInfo: AppInfo | LibInfo | null,
|
|
635
635
|
dict: { [key: string]: string },
|
|
636
|
-
options?: { [key: string]:
|
|
636
|
+
options?: { [key: string]: unknown },
|
|
637
637
|
) => Promise<string | null | { filename: string; content: string }>;
|
|
638
638
|
};
|
|
639
639
|
const result = await getContent.default(scanInfo ?? null, dict, options);
|
|
@@ -686,7 +686,7 @@ export class Executor {
|
|
|
686
686
|
template: string;
|
|
687
687
|
scanInfo?: AppInfo | LibInfo | null;
|
|
688
688
|
dict?: { [key: string]: string };
|
|
689
|
-
options?: { [key: string]:
|
|
689
|
+
options?: { [key: string]: unknown };
|
|
690
690
|
overwrite?: boolean;
|
|
691
691
|
}): Promise<FileContent[]> {
|
|
692
692
|
const templateRoot = await this.#resolveTemplateRoot();
|
|
@@ -752,7 +752,7 @@ export class Executor {
|
|
|
752
752
|
basePath: string;
|
|
753
753
|
template: string;
|
|
754
754
|
dict?: { [key: string]: string };
|
|
755
|
-
options?: { [key: string]:
|
|
755
|
+
options?: { [key: string]: unknown };
|
|
756
756
|
overwrite?: boolean;
|
|
757
757
|
}): Promise<FileContent[]> {
|
|
758
758
|
const dict = {
|
|
@@ -827,10 +827,10 @@ export class Executor {
|
|
|
827
827
|
filePath: string,
|
|
828
828
|
{ fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {},
|
|
829
829
|
): Promise<{
|
|
830
|
-
results:
|
|
830
|
+
results: unknown[]; // ESLint.LintResult[];
|
|
831
831
|
message: string;
|
|
832
|
-
errors:
|
|
833
|
-
warnings:
|
|
832
|
+
errors: unknown[]; // ESLintLinter.LintMessage[];
|
|
833
|
+
warnings: unknown[]; // ESLintLinter.LintMessage[];
|
|
834
834
|
}> {
|
|
835
835
|
const path = this.getPath(filePath);
|
|
836
836
|
const linter = this.getLinter();
|
|
@@ -1225,40 +1225,44 @@ export class SysExecutor extends Executor {
|
|
|
1225
1225
|
async getViewComponents() {
|
|
1226
1226
|
const viewComponents = (await this.readdir("lib"))
|
|
1227
1227
|
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1228
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.View.tsx`).exists());
|
|
1228
|
+
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.View.tsx`).exists());
|
|
1229
1229
|
return viewComponents;
|
|
1230
1230
|
}
|
|
1231
1231
|
|
|
1232
1232
|
async getUnitComponents() {
|
|
1233
1233
|
const unitComponents = (await this.readdir("lib"))
|
|
1234
1234
|
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1235
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.Unit.tsx`).exists());
|
|
1235
|
+
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Unit.tsx`).exists());
|
|
1236
1236
|
return unitComponents;
|
|
1237
1237
|
}
|
|
1238
1238
|
async getTemplateComponents() {
|
|
1239
1239
|
const templateComponents = (await this.readdir("lib"))
|
|
1240
1240
|
.filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
|
|
1241
|
-
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.Template.tsx`).exists());
|
|
1241
|
+
.filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Template.tsx`).exists());
|
|
1242
1242
|
return templateComponents;
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
1245
1245
|
async getViewsSourceCode() {
|
|
1246
1246
|
const viewComponents = await this.getViewComponents();
|
|
1247
1247
|
return Promise.all(
|
|
1248
|
-
viewComponents.map((viewComponent) =>
|
|
1248
|
+
viewComponents.map((viewComponent) =>
|
|
1249
|
+
this.getLocalFile(`lib/${viewComponent}/${capitalize(viewComponent)}.View.tsx`),
|
|
1250
|
+
),
|
|
1249
1251
|
);
|
|
1250
1252
|
}
|
|
1251
1253
|
async getUnitsSourceCode() {
|
|
1252
1254
|
const unitComponents = await this.getUnitComponents();
|
|
1253
1255
|
return Promise.all(
|
|
1254
|
-
unitComponents.map((unitComponent) =>
|
|
1256
|
+
unitComponents.map((unitComponent) =>
|
|
1257
|
+
this.getLocalFile(`lib/${unitComponent}/${capitalize(unitComponent)}.Unit.tsx`),
|
|
1258
|
+
),
|
|
1255
1259
|
);
|
|
1256
1260
|
}
|
|
1257
1261
|
async getTemplatesSourceCode() {
|
|
1258
1262
|
const templateComponents = await this.getTemplateComponents();
|
|
1259
1263
|
return Promise.all(
|
|
1260
1264
|
templateComponents.map((templateComponent) =>
|
|
1261
|
-
this.getLocalFile(`lib/${templateComponent}/${templateComponent}.Template.tsx`),
|
|
1265
|
+
this.getLocalFile(`lib/${templateComponent}/${capitalize(templateComponent)}.Template.tsx`),
|
|
1262
1266
|
),
|
|
1263
1267
|
);
|
|
1264
1268
|
}
|
package/getModelFileData.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { capitalize } from "akanjs/common";
|
|
2
|
+
|
|
1
3
|
interface ModelFileData {
|
|
2
4
|
moduleType: "lib" | "app";
|
|
3
5
|
moduleName: string;
|
|
@@ -16,9 +18,10 @@ interface ModelFileData {
|
|
|
16
18
|
export const getModelFileData = async (modulePath: string, modelName: string): Promise<ModelFileData> => {
|
|
17
19
|
const moduleType = modulePath.startsWith("apps") ? "app" : "lib";
|
|
18
20
|
const moduleName = modulePath.split("/")[1] ?? "";
|
|
21
|
+
const modelComponentName = capitalize(modelName);
|
|
19
22
|
const constantFilePath = `${modulePath}/lib/${modelName}/${modelName}.constant.ts`;
|
|
20
|
-
const unitFilePath = `${modulePath}/lib/${modelName}/${
|
|
21
|
-
const viewFilePath = `${modulePath}/lib/${modelName}/${
|
|
23
|
+
const unitFilePath = `${modulePath}/lib/${modelName}/${modelComponentName}.Unit.tsx`;
|
|
24
|
+
const viewFilePath = `${modulePath}/lib/${modelName}/${modelComponentName}.View.tsx`;
|
|
22
25
|
const [constantFileStr, unitFileStr, viewFileStr] = await Promise.all([
|
|
23
26
|
Bun.file(constantFilePath).text(),
|
|
24
27
|
Bun.file(unitFilePath).text(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.3.9-rc.
|
|
3
|
+
"version": "2.3.9-rc.5",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.3.9-rc.
|
|
35
|
+
"akanjs": "2.3.9-rc.5",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "5.5.23",
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import type { Workspace } from "../commandDecorators";
|
|
2
|
+
import type {
|
|
3
|
+
GeneratedSyncState,
|
|
4
|
+
PrimitiveChangedFile,
|
|
5
|
+
PrimitiveGeneratedFile,
|
|
6
|
+
RepairAction,
|
|
7
|
+
RepairReport,
|
|
8
|
+
WorkflowApplyCommand,
|
|
9
|
+
WorkflowApplyReport,
|
|
10
|
+
WorkflowDiagnostic,
|
|
11
|
+
WorkflowFailureScope,
|
|
12
|
+
WorkflowKnownBlocker,
|
|
13
|
+
WorkflowPlan,
|
|
14
|
+
WorkflowRunArtifact,
|
|
15
|
+
WorkflowRunSource,
|
|
16
|
+
WorkflowValidationCommandResult,
|
|
17
|
+
WorkflowValidationRunReport,
|
|
18
|
+
WorkflowValidationStatus,
|
|
19
|
+
} from "./types";
|
|
20
|
+
import { commandStatus, jsonText, uniqueBy, workflowStatus } from "./utils";
|
|
21
|
+
|
|
22
|
+
export const createWorkflowApplyReport = ({
|
|
23
|
+
workflow,
|
|
24
|
+
mode,
|
|
25
|
+
changedFiles = [],
|
|
26
|
+
generatedFiles = [],
|
|
27
|
+
appliedCommands = [],
|
|
28
|
+
recommendedValidationCommands,
|
|
29
|
+
commands = [],
|
|
30
|
+
diagnostics = [],
|
|
31
|
+
recommendations = [],
|
|
32
|
+
nextActions = [],
|
|
33
|
+
plan,
|
|
34
|
+
}: Omit<
|
|
35
|
+
WorkflowApplyReport,
|
|
36
|
+
| "schemaVersion"
|
|
37
|
+
| "runId"
|
|
38
|
+
| "applyReportPath"
|
|
39
|
+
| "validationTarget"
|
|
40
|
+
| "status"
|
|
41
|
+
| "appliedCommands"
|
|
42
|
+
| "recommendedValidationCommands"
|
|
43
|
+
| "commands"
|
|
44
|
+
| "recommendations"
|
|
45
|
+
> & {
|
|
46
|
+
appliedCommands?: WorkflowApplyCommand[];
|
|
47
|
+
recommendedValidationCommands?: WorkflowApplyCommand[];
|
|
48
|
+
commands?: WorkflowApplyCommand[];
|
|
49
|
+
recommendations?: WorkflowApplyReport["recommendations"];
|
|
50
|
+
}): WorkflowApplyReport => {
|
|
51
|
+
const validationCommands = recommendedValidationCommands ?? commands;
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
workflow,
|
|
55
|
+
mode,
|
|
56
|
+
status: workflowStatus(diagnostics),
|
|
57
|
+
changedFiles: uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
|
|
58
|
+
generatedFiles: uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
|
|
59
|
+
appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
|
|
60
|
+
recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
|
|
61
|
+
commands: uniqueBy(validationCommands, (command) => command.command),
|
|
62
|
+
diagnostics,
|
|
63
|
+
recommendations: uniqueBy(recommendations, (recommendation) => `${recommendation.kind}:${recommendation.code}`),
|
|
64
|
+
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
65
|
+
plan,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
|
|
70
|
+
const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
|
|
71
|
+
return command
|
|
72
|
+
.replaceAll("<app-or-lib-or-pkg>", target)
|
|
73
|
+
.replaceAll("<app-or-lib>", target)
|
|
74
|
+
.replaceAll("<app-name>", target);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
|
|
78
|
+
plan.validation.map((validation) => ({
|
|
79
|
+
command: resolveWorkflowCommand(validation.command, plan),
|
|
80
|
+
reason: validation.reason,
|
|
81
|
+
kind: validation.kind,
|
|
82
|
+
})) satisfies WorkflowApplyCommand[];
|
|
83
|
+
|
|
84
|
+
export const workflowRunsDir = ".akan/workflows/runs";
|
|
85
|
+
|
|
86
|
+
export const createWorkflowRunId = (prefix = "run") =>
|
|
87
|
+
`${prefix}-${new Date()
|
|
88
|
+
.toISOString()
|
|
89
|
+
.replace(/[-:.TZ]/g, "")
|
|
90
|
+
.slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
91
|
+
|
|
92
|
+
const getRunId = (artifact: WorkflowRunArtifact) => {
|
|
93
|
+
if ("runId" in artifact && artifact.runId) return artifact.runId;
|
|
94
|
+
return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
|
|
98
|
+
|
|
99
|
+
const withWorkflowRunMetadata = (
|
|
100
|
+
artifact: WorkflowRunArtifact,
|
|
101
|
+
runId: string,
|
|
102
|
+
artifactPath: string,
|
|
103
|
+
): WorkflowRunArtifact => {
|
|
104
|
+
if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
|
|
105
|
+
return { ...artifact, runId, applyReportPath: artifactPath, validationTarget: artifactPath };
|
|
106
|
+
}
|
|
107
|
+
if ("kind" in artifact) return { ...artifact, runId, repairReportPath: artifactPath };
|
|
108
|
+
return artifact;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
|
|
112
|
+
const runId = getRunId(artifact);
|
|
113
|
+
const artifactPath = workflowRunArtifactPath(runId);
|
|
114
|
+
const artifactWithMetadata = withWorkflowRunMetadata(artifact, runId, artifactPath);
|
|
115
|
+
await workspace.writeFile(artifactPath, jsonText(artifactWithMetadata), { silent: true });
|
|
116
|
+
return { runId, path: artifactPath, artifact: artifactWithMetadata };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const workflowSyncDir = ".akan/workflows/sync";
|
|
120
|
+
|
|
121
|
+
const syncStateSlug = (target: string) =>
|
|
122
|
+
target
|
|
123
|
+
.trim()
|
|
124
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
125
|
+
.replace(/^-+|-+$/g, "")
|
|
126
|
+
.toLowerCase();
|
|
127
|
+
|
|
128
|
+
export const workflowSyncStatePath = (target: string) => `${workflowSyncDir}/${syncStateSlug(target) || "target"}.json`;
|
|
129
|
+
|
|
130
|
+
export const generatedFilePathsForTarget = (targetRoot: string, reason = "Generated files were refreshed by sync.") =>
|
|
131
|
+
[
|
|
132
|
+
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
133
|
+
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
134
|
+
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
135
|
+
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason },
|
|
136
|
+
] satisfies PrimitiveGeneratedFile[];
|
|
137
|
+
|
|
138
|
+
export const writeGeneratedSyncState = async (workspace: Workspace, state: GeneratedSyncState) => {
|
|
139
|
+
const statePath = workflowSyncStatePath(state.target);
|
|
140
|
+
await workspace.writeFile(statePath, jsonText(state), { silent: true });
|
|
141
|
+
return statePath;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
|
|
145
|
+
const artifactPath = workflowRunArtifactPath(runId);
|
|
146
|
+
if (!(await workspace.exists(artifactPath))) throw new Error(`Workflow run artifact does not exist: ${artifactPath}`);
|
|
147
|
+
return (await workspace.readJson(artifactPath)) as WorkflowRunArtifact;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type WorkflowValidationCommandExecutor = (
|
|
151
|
+
command: WorkflowApplyCommand,
|
|
152
|
+
) => Promise<WorkflowValidationCommandResult>;
|
|
153
|
+
|
|
154
|
+
const failedCommandScopes = (commands: readonly WorkflowValidationCommandResult[]) =>
|
|
155
|
+
commands.filter((command) => command.status === "failed").map((command) => command.failureScope ?? "unknown");
|
|
156
|
+
|
|
157
|
+
const errorDiagnosticScopes = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
158
|
+
diagnostics
|
|
159
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
160
|
+
.map(
|
|
161
|
+
(diagnostic) =>
|
|
162
|
+
diagnostic.failureScope ??
|
|
163
|
+
(diagnostic.scope === "baseline"
|
|
164
|
+
? "workspace-config"
|
|
165
|
+
: diagnostic.scope === "workflow"
|
|
166
|
+
? "source-change"
|
|
167
|
+
: "unknown"),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const hasScopeFailure = (
|
|
171
|
+
scopes: readonly WorkflowFailureScope[],
|
|
172
|
+
scope: WorkflowFailureScope,
|
|
173
|
+
diagnostics: readonly WorkflowDiagnostic[] = [],
|
|
174
|
+
) =>
|
|
175
|
+
scopes.includes(scope) ||
|
|
176
|
+
diagnostics.some((diagnostic) => diagnostic.severity === "error" && diagnostic.failureScope === scope);
|
|
177
|
+
|
|
178
|
+
const statusForScope = (
|
|
179
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
180
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
181
|
+
scopes: readonly WorkflowFailureScope[],
|
|
182
|
+
expectedScope: WorkflowFailureScope,
|
|
183
|
+
): WorkflowValidationStatus => {
|
|
184
|
+
const hasCommands = commands.length > 0 || diagnostics.length > 0;
|
|
185
|
+
if (!hasCommands) return "unknown";
|
|
186
|
+
return hasScopeFailure(scopes, expectedScope, diagnostics) ? "failed" : "passed";
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const statusForValidationKind = (
|
|
190
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
191
|
+
kind: WorkflowValidationCommandResult["kind"],
|
|
192
|
+
): WorkflowValidationStatus => {
|
|
193
|
+
const matching = commands.filter((command) => command.kind === kind);
|
|
194
|
+
if (matching.length === 0) return "unknown";
|
|
195
|
+
return matching.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const createKnownBlockers = (
|
|
199
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
200
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
201
|
+
): WorkflowKnownBlocker[] => {
|
|
202
|
+
const commandBlockers = commands
|
|
203
|
+
.filter(
|
|
204
|
+
(command) =>
|
|
205
|
+
command.status === "failed" &&
|
|
206
|
+
(command.failureScope === "workspace-config" || command.failureScope === "environment"),
|
|
207
|
+
)
|
|
208
|
+
.map((command) => ({
|
|
209
|
+
code: `workflow-validation-${command.failureScope}`,
|
|
210
|
+
message: `${command.failureScope === "environment" ? "Environment" : "Workspace configuration"} blocker: ${command.command}`,
|
|
211
|
+
failureScope: command.failureScope ?? "unknown",
|
|
212
|
+
command: command.command,
|
|
213
|
+
kind: command.kind,
|
|
214
|
+
count: 1,
|
|
215
|
+
}));
|
|
216
|
+
const diagnosticBlockers = diagnostics
|
|
217
|
+
.filter(
|
|
218
|
+
(diagnostic) =>
|
|
219
|
+
diagnostic.severity === "error" &&
|
|
220
|
+
(diagnostic.failureScope === "workspace-config" ||
|
|
221
|
+
diagnostic.failureScope === "environment" ||
|
|
222
|
+
diagnostic.scope === "baseline"),
|
|
223
|
+
)
|
|
224
|
+
.map((diagnostic) => ({
|
|
225
|
+
code: diagnostic.code,
|
|
226
|
+
message: diagnostic.message,
|
|
227
|
+
failureScope: diagnostic.failureScope ?? ("workspace-config" as const),
|
|
228
|
+
command: diagnostic.command,
|
|
229
|
+
kind: diagnostic.kind,
|
|
230
|
+
count: 1,
|
|
231
|
+
}));
|
|
232
|
+
const grouped = new Map<string, WorkflowKnownBlocker>();
|
|
233
|
+
for (const blocker of [...commandBlockers, ...diagnosticBlockers]) {
|
|
234
|
+
const key = `${blocker.failureScope}:${blocker.code}:${blocker.command ?? ""}:${blocker.message}`;
|
|
235
|
+
const existing = grouped.get(key);
|
|
236
|
+
if (existing) existing.count += blocker.count;
|
|
237
|
+
else grouped.set(key, blocker);
|
|
238
|
+
}
|
|
239
|
+
return [...grouped.values()];
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const createValidationStatuses = (
|
|
243
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
244
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
245
|
+
) => {
|
|
246
|
+
const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
|
|
247
|
+
const sourceStatus = statusForScope(commands, diagnostics, scopes, "source-change");
|
|
248
|
+
const workspaceStatus =
|
|
249
|
+
hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
|
|
250
|
+
? "failed"
|
|
251
|
+
: commands.length || diagnostics.length
|
|
252
|
+
? "passed"
|
|
253
|
+
: "unknown";
|
|
254
|
+
const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
|
|
255
|
+
? "failed"
|
|
256
|
+
: hasScopeFailure(scopes, "workspace-config", diagnostics)
|
|
257
|
+
? "blocked-by-workspace-config"
|
|
258
|
+
: hasScopeFailure(scopes, "environment", diagnostics)
|
|
259
|
+
? "blocked-by-environment"
|
|
260
|
+
: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
|
|
261
|
+
? "failed"
|
|
262
|
+
: "passed";
|
|
263
|
+
return {
|
|
264
|
+
sourceStatus,
|
|
265
|
+
workspaceStatus,
|
|
266
|
+
overallStatus,
|
|
267
|
+
summary: {
|
|
268
|
+
sourceChange: sourceStatus,
|
|
269
|
+
generatedSync: statusForValidationKind(commands, "sync"),
|
|
270
|
+
workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
|
|
271
|
+
environment: statusForScope(commands, diagnostics, scopes, "environment"),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const createWorkflowValidationRunReport = async ({
|
|
277
|
+
runId = createWorkflowRunId("validation"),
|
|
278
|
+
workflow,
|
|
279
|
+
source,
|
|
280
|
+
plan,
|
|
281
|
+
commands,
|
|
282
|
+
execute,
|
|
283
|
+
diagnostics = [],
|
|
284
|
+
baselineDiagnostics = [],
|
|
285
|
+
workflowDiagnostics = [],
|
|
286
|
+
repairActions = [],
|
|
287
|
+
}: {
|
|
288
|
+
runId?: string;
|
|
289
|
+
workflow: string;
|
|
290
|
+
source: WorkflowRunSource;
|
|
291
|
+
plan?: WorkflowPlan;
|
|
292
|
+
commands: WorkflowApplyCommand[];
|
|
293
|
+
execute: WorkflowValidationCommandExecutor;
|
|
294
|
+
diagnostics?: WorkflowDiagnostic[];
|
|
295
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
296
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
297
|
+
repairActions?: RepairAction[];
|
|
298
|
+
}): Promise<WorkflowValidationRunReport> => {
|
|
299
|
+
const results: WorkflowValidationCommandResult[] = [];
|
|
300
|
+
for (const command of commands) {
|
|
301
|
+
results.push(await execute(command));
|
|
302
|
+
}
|
|
303
|
+
const commandDiagnostics = results.flatMap((result) =>
|
|
304
|
+
result.status === "failed"
|
|
305
|
+
? [
|
|
306
|
+
{
|
|
307
|
+
severity: "error" as const,
|
|
308
|
+
code: "workflow-validation-command-failed",
|
|
309
|
+
message: `Validation command failed: ${result.command}`,
|
|
310
|
+
command: result.command,
|
|
311
|
+
kind: result.kind,
|
|
312
|
+
failureScope: result.failureScope,
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
: [],
|
|
316
|
+
);
|
|
317
|
+
const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
|
|
318
|
+
const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
319
|
+
const statuses = createValidationStatuses(results, scopedDiagnostics);
|
|
320
|
+
return {
|
|
321
|
+
schemaVersion: 1,
|
|
322
|
+
runId,
|
|
323
|
+
workflow,
|
|
324
|
+
mode: "validate",
|
|
325
|
+
source,
|
|
326
|
+
status: statuses.overallStatus === "passed" ? "passed" : "failed",
|
|
327
|
+
...statuses,
|
|
328
|
+
knownBlockers: createKnownBlockers(results, scopedDiagnostics),
|
|
329
|
+
commands: results,
|
|
330
|
+
diagnostics: reportDiagnostics,
|
|
331
|
+
baselineDiagnostics,
|
|
332
|
+
workflowDiagnostics,
|
|
333
|
+
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
334
|
+
nextActions: results
|
|
335
|
+
.filter((result) => result.status === "failed")
|
|
336
|
+
.map((result) => ({ command: result.command, reason: "Re-run this validation command after repair." })),
|
|
337
|
+
plan,
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
342
|
+
const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
343
|
+
if (change.action !== "create" && change.action !== "modify") return [];
|
|
344
|
+
return [
|
|
345
|
+
{
|
|
346
|
+
path: change.target,
|
|
347
|
+
action: change.action,
|
|
348
|
+
reason: change.reason,
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
});
|
|
352
|
+
const generatedFiles: PrimitiveGeneratedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
353
|
+
if (change.action !== "sync") return [];
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
path: change.target,
|
|
357
|
+
action: "sync",
|
|
358
|
+
reason: change.reason,
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
});
|
|
362
|
+
const diagnostics = [...plan.diagnostics];
|
|
363
|
+
return createWorkflowApplyReport({
|
|
364
|
+
workflow: plan.workflow,
|
|
365
|
+
mode: "dry-run",
|
|
366
|
+
changedFiles,
|
|
367
|
+
generatedFiles,
|
|
368
|
+
commands: workflowCommandsForPlan(plan),
|
|
369
|
+
diagnostics,
|
|
370
|
+
recommendations: plan.recommendations,
|
|
371
|
+
nextActions: workflowCommandsForPlan(plan),
|
|
372
|
+
plan,
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export const createRepairReport = ({
|
|
377
|
+
command,
|
|
378
|
+
kind,
|
|
379
|
+
target = null,
|
|
380
|
+
diagnostics = [],
|
|
381
|
+
repairActions = [],
|
|
382
|
+
nextActions = [],
|
|
383
|
+
commands = [],
|
|
384
|
+
generatedFiles = [],
|
|
385
|
+
syncedAt,
|
|
386
|
+
}: Omit<
|
|
387
|
+
RepairReport,
|
|
388
|
+
| "schemaVersion"
|
|
389
|
+
| "runId"
|
|
390
|
+
| "repairReportPath"
|
|
391
|
+
| "status"
|
|
392
|
+
| "target"
|
|
393
|
+
| "diagnostics"
|
|
394
|
+
| "repairActions"
|
|
395
|
+
| "nextActions"
|
|
396
|
+
| "commands"
|
|
397
|
+
> &
|
|
398
|
+
Partial<
|
|
399
|
+
Pick<
|
|
400
|
+
RepairReport,
|
|
401
|
+
"target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
|
|
402
|
+
>
|
|
403
|
+
>): RepairReport => ({
|
|
404
|
+
schemaVersion: 1,
|
|
405
|
+
command,
|
|
406
|
+
kind,
|
|
407
|
+
target,
|
|
408
|
+
status: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed" ? "failed" : "passed",
|
|
409
|
+
diagnostics,
|
|
410
|
+
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
411
|
+
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
412
|
+
commands,
|
|
413
|
+
generatedFiles,
|
|
414
|
+
...(syncedAt ? { syncedAt } : {}),
|
|
415
|
+
});
|