@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 CHANGED
@@ -713,8 +713,14 @@ export class AkanContextAnalyzer {
713
713
  };
714
714
  }
715
715
 
716
- static findModules(context: AkanWorkspaceContext, moduleName?: string | null) {
717
- const modules = [...context.apps, ...context.libs].flatMap((sys) => sys.modules);
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",
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.3",
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
+ }