@akanjs/devkit 2.3.9-rc.2 → 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.
@@ -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
+ }