@akanjs/devkit 2.3.9-rc.3 → 2.3.9-rc.4
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/package.json +2 -2
- package/workflow/artifacts.ts +396 -0
- package/workflow/executor.ts +258 -0
- package/workflow/index.ts +8 -1399
- package/workflow/plan.ts +210 -0
- package/workflow/primitive.ts +59 -0
- package/workflow/render.ts +215 -0
- package/workflow/source.ts +224 -0
- package/workflow/types.ts +283 -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/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.4",
|
|
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.4",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "5.5.23",
|
|
@@ -0,0 +1,396 @@
|
|
|
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 createKnownBlockers = (
|
|
190
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
191
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
192
|
+
): WorkflowKnownBlocker[] => {
|
|
193
|
+
const commandBlockers = commands
|
|
194
|
+
.filter(
|
|
195
|
+
(command) =>
|
|
196
|
+
command.status === "failed" &&
|
|
197
|
+
(command.failureScope === "workspace-config" || command.failureScope === "environment"),
|
|
198
|
+
)
|
|
199
|
+
.map((command) => ({
|
|
200
|
+
code: `workflow-validation-${command.failureScope}`,
|
|
201
|
+
message: `${command.failureScope === "environment" ? "Environment" : "Workspace configuration"} blocker: ${command.command}`,
|
|
202
|
+
failureScope: command.failureScope ?? "unknown",
|
|
203
|
+
command: command.command,
|
|
204
|
+
kind: command.kind,
|
|
205
|
+
count: 1,
|
|
206
|
+
}));
|
|
207
|
+
const diagnosticBlockers = diagnostics
|
|
208
|
+
.filter(
|
|
209
|
+
(diagnostic) =>
|
|
210
|
+
diagnostic.severity === "error" &&
|
|
211
|
+
(diagnostic.failureScope === "workspace-config" ||
|
|
212
|
+
diagnostic.failureScope === "environment" ||
|
|
213
|
+
diagnostic.scope === "baseline"),
|
|
214
|
+
)
|
|
215
|
+
.map((diagnostic) => ({
|
|
216
|
+
code: diagnostic.code,
|
|
217
|
+
message: diagnostic.message,
|
|
218
|
+
failureScope: diagnostic.failureScope ?? ("workspace-config" as const),
|
|
219
|
+
command: diagnostic.command,
|
|
220
|
+
kind: diagnostic.kind,
|
|
221
|
+
count: 1,
|
|
222
|
+
}));
|
|
223
|
+
const grouped = new Map<string, WorkflowKnownBlocker>();
|
|
224
|
+
for (const blocker of [...commandBlockers, ...diagnosticBlockers]) {
|
|
225
|
+
const key = `${blocker.failureScope}:${blocker.code}:${blocker.command ?? ""}:${blocker.message}`;
|
|
226
|
+
const existing = grouped.get(key);
|
|
227
|
+
if (existing) existing.count += blocker.count;
|
|
228
|
+
else grouped.set(key, blocker);
|
|
229
|
+
}
|
|
230
|
+
return [...grouped.values()];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const createValidationStatuses = (
|
|
234
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
235
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
236
|
+
) => {
|
|
237
|
+
const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
|
|
238
|
+
const sourceStatus = statusForScope(commands, diagnostics, scopes, "source-change");
|
|
239
|
+
const workspaceStatus =
|
|
240
|
+
hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
|
|
241
|
+
? "failed"
|
|
242
|
+
: commands.length || diagnostics.length
|
|
243
|
+
? "passed"
|
|
244
|
+
: "unknown";
|
|
245
|
+
const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
|
|
246
|
+
? "failed"
|
|
247
|
+
: hasScopeFailure(scopes, "workspace-config", diagnostics)
|
|
248
|
+
? "blocked-by-workspace-config"
|
|
249
|
+
: hasScopeFailure(scopes, "environment", diagnostics)
|
|
250
|
+
? "blocked-by-environment"
|
|
251
|
+
: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
|
|
252
|
+
? "failed"
|
|
253
|
+
: "passed";
|
|
254
|
+
return { sourceStatus, workspaceStatus, overallStatus };
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const createWorkflowValidationRunReport = async ({
|
|
258
|
+
runId = createWorkflowRunId("validation"),
|
|
259
|
+
workflow,
|
|
260
|
+
source,
|
|
261
|
+
plan,
|
|
262
|
+
commands,
|
|
263
|
+
execute,
|
|
264
|
+
diagnostics = [],
|
|
265
|
+
baselineDiagnostics = [],
|
|
266
|
+
workflowDiagnostics = [],
|
|
267
|
+
repairActions = [],
|
|
268
|
+
}: {
|
|
269
|
+
runId?: string;
|
|
270
|
+
workflow: string;
|
|
271
|
+
source: WorkflowRunSource;
|
|
272
|
+
plan?: WorkflowPlan;
|
|
273
|
+
commands: WorkflowApplyCommand[];
|
|
274
|
+
execute: WorkflowValidationCommandExecutor;
|
|
275
|
+
diagnostics?: WorkflowDiagnostic[];
|
|
276
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
277
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
278
|
+
repairActions?: RepairAction[];
|
|
279
|
+
}): Promise<WorkflowValidationRunReport> => {
|
|
280
|
+
const results: WorkflowValidationCommandResult[] = [];
|
|
281
|
+
for (const command of commands) {
|
|
282
|
+
results.push(await execute(command));
|
|
283
|
+
}
|
|
284
|
+
const commandDiagnostics = results.flatMap((result) =>
|
|
285
|
+
result.status === "failed"
|
|
286
|
+
? [
|
|
287
|
+
{
|
|
288
|
+
severity: "error" as const,
|
|
289
|
+
code: "workflow-validation-command-failed",
|
|
290
|
+
message: `Validation command failed: ${result.command}`,
|
|
291
|
+
command: result.command,
|
|
292
|
+
kind: result.kind,
|
|
293
|
+
failureScope: result.failureScope,
|
|
294
|
+
},
|
|
295
|
+
]
|
|
296
|
+
: [],
|
|
297
|
+
);
|
|
298
|
+
const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
|
|
299
|
+
const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
300
|
+
const statuses = createValidationStatuses(results, scopedDiagnostics);
|
|
301
|
+
return {
|
|
302
|
+
schemaVersion: 1,
|
|
303
|
+
runId,
|
|
304
|
+
workflow,
|
|
305
|
+
mode: "validate",
|
|
306
|
+
source,
|
|
307
|
+
status: statuses.overallStatus === "passed" ? "passed" : "failed",
|
|
308
|
+
...statuses,
|
|
309
|
+
knownBlockers: createKnownBlockers(results, scopedDiagnostics),
|
|
310
|
+
commands: results,
|
|
311
|
+
diagnostics: reportDiagnostics,
|
|
312
|
+
baselineDiagnostics,
|
|
313
|
+
workflowDiagnostics,
|
|
314
|
+
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
315
|
+
nextActions: results
|
|
316
|
+
.filter((result) => result.status === "failed")
|
|
317
|
+
.map((result) => ({ command: result.command, reason: "Re-run this validation command after repair." })),
|
|
318
|
+
plan,
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
323
|
+
const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
324
|
+
if (change.action !== "create" && change.action !== "modify") return [];
|
|
325
|
+
return [
|
|
326
|
+
{
|
|
327
|
+
path: change.target,
|
|
328
|
+
action: change.action,
|
|
329
|
+
reason: change.reason,
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
});
|
|
333
|
+
const generatedFiles: PrimitiveGeneratedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
334
|
+
if (change.action !== "sync") return [];
|
|
335
|
+
return [
|
|
336
|
+
{
|
|
337
|
+
path: change.target,
|
|
338
|
+
action: "sync",
|
|
339
|
+
reason: change.reason,
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
});
|
|
343
|
+
const diagnostics = [...plan.diagnostics];
|
|
344
|
+
return createWorkflowApplyReport({
|
|
345
|
+
workflow: plan.workflow,
|
|
346
|
+
mode: "dry-run",
|
|
347
|
+
changedFiles,
|
|
348
|
+
generatedFiles,
|
|
349
|
+
commands: workflowCommandsForPlan(plan),
|
|
350
|
+
diagnostics,
|
|
351
|
+
recommendations: plan.recommendations,
|
|
352
|
+
nextActions: workflowCommandsForPlan(plan),
|
|
353
|
+
plan,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export const createRepairReport = ({
|
|
358
|
+
command,
|
|
359
|
+
kind,
|
|
360
|
+
target = null,
|
|
361
|
+
diagnostics = [],
|
|
362
|
+
repairActions = [],
|
|
363
|
+
nextActions = [],
|
|
364
|
+
commands = [],
|
|
365
|
+
generatedFiles = [],
|
|
366
|
+
syncedAt,
|
|
367
|
+
}: Omit<
|
|
368
|
+
RepairReport,
|
|
369
|
+
| "schemaVersion"
|
|
370
|
+
| "runId"
|
|
371
|
+
| "repairReportPath"
|
|
372
|
+
| "status"
|
|
373
|
+
| "target"
|
|
374
|
+
| "diagnostics"
|
|
375
|
+
| "repairActions"
|
|
376
|
+
| "nextActions"
|
|
377
|
+
| "commands"
|
|
378
|
+
> &
|
|
379
|
+
Partial<
|
|
380
|
+
Pick<
|
|
381
|
+
RepairReport,
|
|
382
|
+
"target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
|
|
383
|
+
>
|
|
384
|
+
>): RepairReport => ({
|
|
385
|
+
schemaVersion: 1,
|
|
386
|
+
command,
|
|
387
|
+
kind,
|
|
388
|
+
target,
|
|
389
|
+
status: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed" ? "failed" : "passed",
|
|
390
|
+
diagnostics,
|
|
391
|
+
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
392
|
+
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
393
|
+
commands,
|
|
394
|
+
generatedFiles,
|
|
395
|
+
...(syncedAt ? { syncedAt } : {}),
|
|
396
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { capitalize } from "akanjs/common";
|
|
2
|
+
import type { Sys, Workspace } from "../commandDecorators";
|
|
3
|
+
import { AppExecutor, LibExecutor } from "../executors";
|
|
4
|
+
import { createWorkflowApplyReport, workflowCommandsForPlan } from "./artifacts";
|
|
5
|
+
import type {
|
|
6
|
+
PrimitiveChangedFile,
|
|
7
|
+
PrimitiveGeneratedFile,
|
|
8
|
+
PrimitiveNextAction,
|
|
9
|
+
PrimitiveWriteReport,
|
|
10
|
+
WorkflowApplyCommand,
|
|
11
|
+
WorkflowDiagnostic,
|
|
12
|
+
WorkflowInputValue,
|
|
13
|
+
WorkflowPlan,
|
|
14
|
+
WorkflowPrimitiveOperations,
|
|
15
|
+
WorkflowStep,
|
|
16
|
+
WorkflowStepRegistry,
|
|
17
|
+
WorkflowStepResult,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
export const workflowStepKey = (workflow: string, stepId: string) => `${workflow}:${stepId}`;
|
|
21
|
+
|
|
22
|
+
export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport): WorkflowStepResult => ({
|
|
23
|
+
changedFiles: report.changedFiles,
|
|
24
|
+
generatedFiles: report.generatedFiles,
|
|
25
|
+
commands: report.validationCommands,
|
|
26
|
+
diagnostics: report.diagnostics,
|
|
27
|
+
recommendations: [],
|
|
28
|
+
nextActions: report.nextActions,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const workflowStringInput = (value: WorkflowInputValue | undefined) => (typeof value === "string" ? value : null);
|
|
32
|
+
const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
|
|
33
|
+
Array.isArray(value) ? value.join(",") : null;
|
|
34
|
+
|
|
35
|
+
const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
36
|
+
if (!target) return null;
|
|
37
|
+
const [apps, libs] = await workspace.getSyss();
|
|
38
|
+
if (apps.includes(target)) return AppExecutor.from(workspace, target);
|
|
39
|
+
if (libs.includes(target)) return LibExecutor.from(workspace, target);
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const targetMissing = (input = "app"): WorkflowDiagnostic => ({
|
|
44
|
+
severity: "error",
|
|
45
|
+
code: "workflow-target-missing",
|
|
46
|
+
input,
|
|
47
|
+
message: "Workflow target app or library was not found.",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const inputMissing = (input: string): WorkflowDiagnostic => ({
|
|
51
|
+
severity: "error",
|
|
52
|
+
code: "workflow-input-missing",
|
|
53
|
+
input,
|
|
54
|
+
message: `Workflow input "${input}" is required for apply.`,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const unsupportedInput = (input: string, message: string): WorkflowDiagnostic => ({
|
|
58
|
+
severity: "error",
|
|
59
|
+
code: "workflow-input-unsupported",
|
|
60
|
+
input,
|
|
61
|
+
message,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
|
|
65
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
66
|
+
const module = workflowStringInput(plan.inputs.module) ?? "<module>";
|
|
67
|
+
const field = workflowStringInput(plan.inputs.field) ?? "<field>";
|
|
68
|
+
const typeName = workflowStringInput(plan.inputs.type);
|
|
69
|
+
const isNumeric = typeName === "Int" || typeName === "Float" || typeName === "integer" || typeName === "float";
|
|
70
|
+
const moduleClassName = capitalize(module);
|
|
71
|
+
const target = `${app ? `apps/${app}` : "*"}/lib/${module}/${moduleClassName}.Template.tsx`;
|
|
72
|
+
return {
|
|
73
|
+
recommendations: [
|
|
74
|
+
{
|
|
75
|
+
code: "add-field-ui-surface-review",
|
|
76
|
+
kind: "manual-action",
|
|
77
|
+
target,
|
|
78
|
+
action: isNumeric
|
|
79
|
+
? `Review ${moduleClassName} Template/Unit/View/Store surfaces before adding ${field}; confirm the local Field.Text numeric parser/formatter, validation rule, and dictionary label pattern.`
|
|
80
|
+
: `Add ${field} to ${moduleClassName} UI surfaces only when the local Field.* pattern is clear.`,
|
|
81
|
+
confidence: "medium",
|
|
82
|
+
message: isNumeric
|
|
83
|
+
? `No UI file was modified automatically because a safe numeric input component pattern is not yet detected for ${module}.${field}.`
|
|
84
|
+
: `Review Template/Unit/View/Store surfaces for ${module}.${field}; no UI file was modified automatically.`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
nextActions: [
|
|
88
|
+
{
|
|
89
|
+
command: `akan workflow explain ${plan.workflow}`,
|
|
90
|
+
reason: "Review UI surface guidance before manually editing ambiguous UI files.",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const createWorkflowStepRegistry = ({
|
|
97
|
+
workspace,
|
|
98
|
+
createModule,
|
|
99
|
+
createScalar,
|
|
100
|
+
createUi,
|
|
101
|
+
addField,
|
|
102
|
+
addEnumField,
|
|
103
|
+
}: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
|
|
104
|
+
const inspect = async () => undefined;
|
|
105
|
+
const commandOnly = async () => undefined;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
inspectSystem: inspect,
|
|
109
|
+
inspectModule: inspect,
|
|
110
|
+
syncTarget: commandOnly,
|
|
111
|
+
lintTarget: commandOnly,
|
|
112
|
+
[workflowStepKey("create-module", "create-module")]: async (_step, plan) => {
|
|
113
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
114
|
+
const module = workflowStringInput(plan.inputs.module);
|
|
115
|
+
const sys = await resolveWorkflowSys(workspace, app);
|
|
116
|
+
if (!sys || !module) return { diagnostics: [!sys ? targetMissing() : inputMissing("module")] };
|
|
117
|
+
return primitiveReportToWorkflowStepResult(await createModule(sys, module));
|
|
118
|
+
},
|
|
119
|
+
[workflowStepKey("create-scalar", "create-scalar")]: async (_step, plan) => {
|
|
120
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
121
|
+
const scalar = workflowStringInput(plan.inputs.scalar);
|
|
122
|
+
const sys = await resolveWorkflowSys(workspace, app);
|
|
123
|
+
if (!sys || !scalar) return { diagnostics: [!sys ? targetMissing() : inputMissing("scalar")] };
|
|
124
|
+
return primitiveReportToWorkflowStepResult(await createScalar(sys, scalar));
|
|
125
|
+
},
|
|
126
|
+
[workflowStepKey("create-ui", "create-ui")]: async (_step, plan) => {
|
|
127
|
+
const surface = workflowStringInput(plan.inputs.surface);
|
|
128
|
+
if (surface !== "view" && surface !== "unit" && surface !== "template") {
|
|
129
|
+
return {
|
|
130
|
+
diagnostics: [
|
|
131
|
+
unsupportedInput("surface", "Workflow apply currently supports create-ui surfaces: view, unit, template."),
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return primitiveReportToWorkflowStepResult(
|
|
136
|
+
await createUi({
|
|
137
|
+
app: workflowStringInput(plan.inputs.app),
|
|
138
|
+
module: workflowStringInput(plan.inputs.module),
|
|
139
|
+
surface,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
[workflowStepKey("add-field", "update-constant")]: async (_step, plan) => {
|
|
144
|
+
if (workflowStringInput(plan.inputs.type)?.toLowerCase() === "enum") {
|
|
145
|
+
return primitiveReportToWorkflowStepResult(
|
|
146
|
+
await addEnumField({
|
|
147
|
+
app: workflowStringInput(plan.inputs.app),
|
|
148
|
+
module: workflowStringInput(plan.inputs.module),
|
|
149
|
+
field: workflowStringInput(plan.inputs.field),
|
|
150
|
+
values: workflowStringListInput(plan.inputs.values),
|
|
151
|
+
defaultValue: workflowStringInput(plan.inputs.default),
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return primitiveReportToWorkflowStepResult(
|
|
156
|
+
await addField({
|
|
157
|
+
app: workflowStringInput(plan.inputs.app),
|
|
158
|
+
module: workflowStringInput(plan.inputs.module),
|
|
159
|
+
field: workflowStringInput(plan.inputs.field),
|
|
160
|
+
type: workflowStringInput(plan.inputs.type),
|
|
161
|
+
defaultValue: workflowStringInput(plan.inputs.default),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
[workflowStepKey("add-field", "update-dictionary")]: inspect,
|
|
166
|
+
[workflowStepKey("add-field", "update-ui-surfaces")]: async (_step, plan) => addFieldUiSurfaceInspection(plan),
|
|
167
|
+
[workflowStepKey("add-enum-field", "update-constant")]: async (_step, plan) =>
|
|
168
|
+
primitiveReportToWorkflowStepResult(
|
|
169
|
+
await addEnumField({
|
|
170
|
+
app: workflowStringInput(plan.inputs.app),
|
|
171
|
+
module: workflowStringInput(plan.inputs.module),
|
|
172
|
+
field: workflowStringInput(plan.inputs.field),
|
|
173
|
+
values: workflowStringListInput(plan.inputs.values),
|
|
174
|
+
defaultValue: workflowStringInput(plan.inputs.default),
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
178
|
+
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const createWorkflowStepCommandResult = (
|
|
183
|
+
step: WorkflowStep,
|
|
184
|
+
command: string,
|
|
185
|
+
reason: string,
|
|
186
|
+
): WorkflowStepResult => ({
|
|
187
|
+
commands: [{ command, reason, stepId: step.id }],
|
|
188
|
+
nextActions: [{ command, reason }],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export class WorkflowExecutor {
|
|
192
|
+
constructor(private readonly registry: WorkflowStepRegistry) {}
|
|
193
|
+
|
|
194
|
+
async apply(plan: WorkflowPlan) {
|
|
195
|
+
const changedFiles: PrimitiveChangedFile[] = [];
|
|
196
|
+
const generatedFiles: PrimitiveGeneratedFile[] = [];
|
|
197
|
+
const recommendedValidationCommands: WorkflowApplyCommand[] = [];
|
|
198
|
+
const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
|
|
199
|
+
const recommendations = [...plan.recommendations];
|
|
200
|
+
const nextActions: PrimitiveNextAction[] = [];
|
|
201
|
+
|
|
202
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
203
|
+
return createWorkflowApplyReport({
|
|
204
|
+
workflow: plan.workflow,
|
|
205
|
+
mode: "apply",
|
|
206
|
+
changedFiles,
|
|
207
|
+
generatedFiles,
|
|
208
|
+
recommendedValidationCommands,
|
|
209
|
+
diagnostics,
|
|
210
|
+
recommendations,
|
|
211
|
+
nextActions,
|
|
212
|
+
plan,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
recommendedValidationCommands.push(...workflowCommandsForPlan(plan));
|
|
217
|
+
nextActions.push(...workflowCommandsForPlan(plan));
|
|
218
|
+
|
|
219
|
+
for (const step of plan.steps) {
|
|
220
|
+
const runner =
|
|
221
|
+
this.registry[workflowStepKey(plan.workflow, step.id)] ?? this.registry[step.tool] ?? this.registry[step.id];
|
|
222
|
+
if (!runner) {
|
|
223
|
+
diagnostics.push({
|
|
224
|
+
severity: "error",
|
|
225
|
+
code: "workflow-step-unsupported",
|
|
226
|
+
message: `Workflow ${plan.workflow} step "${step.id}" is not supported by workflow apply yet.`,
|
|
227
|
+
});
|
|
228
|
+
nextActions.push({
|
|
229
|
+
command: `akan workflow explain ${plan.workflow}`,
|
|
230
|
+
reason: "Review the unsupported workflow step before retrying apply.",
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = await runner(step, plan);
|
|
236
|
+
if (!result) continue;
|
|
237
|
+
changedFiles.push(...(result.changedFiles ?? []));
|
|
238
|
+
generatedFiles.push(...(result.generatedFiles ?? []));
|
|
239
|
+
recommendedValidationCommands.push(...(result.commands ?? []));
|
|
240
|
+
diagnostics.push(...(result.diagnostics ?? []));
|
|
241
|
+
recommendations.push(...(result.recommendations ?? []));
|
|
242
|
+
nextActions.push(...(result.nextActions ?? []));
|
|
243
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return createWorkflowApplyReport({
|
|
247
|
+
workflow: plan.workflow,
|
|
248
|
+
mode: "apply",
|
|
249
|
+
changedFiles,
|
|
250
|
+
generatedFiles,
|
|
251
|
+
recommendedValidationCommands,
|
|
252
|
+
diagnostics,
|
|
253
|
+
recommendations,
|
|
254
|
+
nextActions,
|
|
255
|
+
plan,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|