@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
|
@@ -0,0 +1,266 @@
|
|
|
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 { moduleSourcePaths } from "./source";
|
|
6
|
+
import type {
|
|
7
|
+
PrimitiveChangedFile,
|
|
8
|
+
PrimitiveGeneratedFile,
|
|
9
|
+
PrimitiveNextAction,
|
|
10
|
+
PrimitiveWriteReport,
|
|
11
|
+
WorkflowApplyCommand,
|
|
12
|
+
WorkflowDiagnostic,
|
|
13
|
+
WorkflowInputValue,
|
|
14
|
+
WorkflowPlan,
|
|
15
|
+
WorkflowPrimitiveOperations,
|
|
16
|
+
WorkflowStep,
|
|
17
|
+
WorkflowStepRegistry,
|
|
18
|
+
WorkflowStepResult,
|
|
19
|
+
} from "./types";
|
|
20
|
+
import { addFieldUiPolicyForType } from "./uiPolicy";
|
|
21
|
+
|
|
22
|
+
export const workflowStepKey = (workflow: string, stepId: string) => `${workflow}:${stepId}`;
|
|
23
|
+
|
|
24
|
+
export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport): WorkflowStepResult => ({
|
|
25
|
+
changedFiles: report.changedFiles,
|
|
26
|
+
generatedFiles: report.generatedFiles,
|
|
27
|
+
commands: report.validationCommands,
|
|
28
|
+
diagnostics: report.diagnostics,
|
|
29
|
+
recommendations: [],
|
|
30
|
+
nextActions: report.nextActions,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const workflowStringInput = (value: WorkflowInputValue | undefined) => (typeof value === "string" ? value : null);
|
|
34
|
+
const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
|
|
35
|
+
Array.isArray(value) ? value.join(",") : null;
|
|
36
|
+
const workflowStringArrayInput = (value: WorkflowInputValue | undefined) => (Array.isArray(value) ? value : null);
|
|
37
|
+
const workflowBooleanInput = (value: WorkflowInputValue | undefined) => (typeof value === "boolean" ? value : null);
|
|
38
|
+
|
|
39
|
+
const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
40
|
+
if (!target) return null;
|
|
41
|
+
const [apps, libs] = await workspace.getSyss();
|
|
42
|
+
if (apps.includes(target)) return AppExecutor.from(workspace, target);
|
|
43
|
+
if (libs.includes(target)) return LibExecutor.from(workspace, target);
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const targetMissing = (input = "app"): WorkflowDiagnostic => ({
|
|
48
|
+
severity: "error",
|
|
49
|
+
code: "workflow-target-missing",
|
|
50
|
+
input,
|
|
51
|
+
message: "Workflow target app or library was not found.",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const inputMissing = (input: string): WorkflowDiagnostic => ({
|
|
55
|
+
severity: "error",
|
|
56
|
+
code: "workflow-input-missing",
|
|
57
|
+
input,
|
|
58
|
+
message: `Workflow input "${input}" is required for apply.`,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const unsupportedInput = (input: string, message: string): WorkflowDiagnostic => ({
|
|
62
|
+
severity: "error",
|
|
63
|
+
code: "workflow-input-unsupported",
|
|
64
|
+
input,
|
|
65
|
+
message,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
|
|
69
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
70
|
+
const module = workflowStringInput(plan.inputs.module) ?? "<module>";
|
|
71
|
+
const field = workflowStringInput(plan.inputs.field) ?? "<field>";
|
|
72
|
+
const typeName = workflowStringInput(plan.inputs.type);
|
|
73
|
+
const policy = addFieldUiPolicyForType(typeName ?? "String");
|
|
74
|
+
const surfaces = workflowStringArrayInput(plan.inputs.surfaces);
|
|
75
|
+
const templateRequested = surfaces?.includes("template") ?? false;
|
|
76
|
+
const moduleClassName = capitalize(module);
|
|
77
|
+
const target = `${app ? `apps/${app}` : "*"}/${moduleSourcePaths(module).template}`;
|
|
78
|
+
return {
|
|
79
|
+
recommendations: [
|
|
80
|
+
{
|
|
81
|
+
code: "add-field-ui-surface-review",
|
|
82
|
+
kind: "manual-action",
|
|
83
|
+
target,
|
|
84
|
+
action: templateRequested
|
|
85
|
+
? `Template was requested for ${field}. If no Template file changed, auto-edit was skipped because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Candidate position: inside Layout.Template near the existing Field components.`
|
|
86
|
+
: `Template was not selected, so UI files are intentionally left unchanged. Candidate positions if you expose it later: Layout.Template field list for editing, Light${moduleClassName} projection for list/card data, and Unit/View card sections for display.`,
|
|
87
|
+
confidence: "medium",
|
|
88
|
+
message: `Review UI surfaces for ${module}.${field}; recommended component is ${policy.component}, with manual reasons and candidate positions in action.`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
nextActions: [
|
|
92
|
+
{
|
|
93
|
+
command: `akan workflow explain ${plan.workflow}`,
|
|
94
|
+
reason: "Review UI surface guidance before manually editing ambiguous UI files.",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const createWorkflowStepRegistry = ({
|
|
101
|
+
workspace,
|
|
102
|
+
createModule,
|
|
103
|
+
createScalar,
|
|
104
|
+
createUi,
|
|
105
|
+
addField,
|
|
106
|
+
addEnumField,
|
|
107
|
+
}: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
|
|
108
|
+
const inspect = async () => undefined;
|
|
109
|
+
const commandOnly = async () => undefined;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
inspectSystem: inspect,
|
|
113
|
+
inspectModule: inspect,
|
|
114
|
+
syncTarget: commandOnly,
|
|
115
|
+
lintTarget: commandOnly,
|
|
116
|
+
[workflowStepKey("create-module", "create-module")]: async (_step, plan) => {
|
|
117
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
118
|
+
const module = workflowStringInput(plan.inputs.module);
|
|
119
|
+
const sys = await resolveWorkflowSys(workspace, app);
|
|
120
|
+
if (!sys || !module) return { diagnostics: [!sys ? targetMissing() : inputMissing("module")] };
|
|
121
|
+
return primitiveReportToWorkflowStepResult(await createModule(sys, module));
|
|
122
|
+
},
|
|
123
|
+
[workflowStepKey("create-scalar", "create-scalar")]: async (_step, plan) => {
|
|
124
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
125
|
+
const scalar = workflowStringInput(plan.inputs.scalar);
|
|
126
|
+
const sys = await resolveWorkflowSys(workspace, app);
|
|
127
|
+
if (!sys || !scalar) return { diagnostics: [!sys ? targetMissing() : inputMissing("scalar")] };
|
|
128
|
+
return primitiveReportToWorkflowStepResult(await createScalar(sys, scalar));
|
|
129
|
+
},
|
|
130
|
+
[workflowStepKey("create-ui", "create-ui")]: async (_step, plan) => {
|
|
131
|
+
const surface = workflowStringInput(plan.inputs.surface);
|
|
132
|
+
if (surface !== "view" && surface !== "unit" && surface !== "template") {
|
|
133
|
+
return {
|
|
134
|
+
diagnostics: [
|
|
135
|
+
unsupportedInput("surface", "Workflow apply currently supports create-ui surfaces: view, unit, template."),
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return primitiveReportToWorkflowStepResult(
|
|
140
|
+
await createUi({
|
|
141
|
+
app: workflowStringInput(plan.inputs.app),
|
|
142
|
+
module: workflowStringInput(plan.inputs.module),
|
|
143
|
+
surface,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
[workflowStepKey("add-field", "update-constant")]: async (_step, plan) => {
|
|
148
|
+
if (workflowStringInput(plan.inputs.type)?.toLowerCase() === "enum") {
|
|
149
|
+
return primitiveReportToWorkflowStepResult(
|
|
150
|
+
await addEnumField({
|
|
151
|
+
app: workflowStringInput(plan.inputs.app),
|
|
152
|
+
module: workflowStringInput(plan.inputs.module),
|
|
153
|
+
field: workflowStringInput(plan.inputs.field),
|
|
154
|
+
values: workflowStringListInput(plan.inputs.values),
|
|
155
|
+
defaultValue: workflowStringInput(plan.inputs.default),
|
|
156
|
+
surfaces: workflowStringArrayInput(plan.inputs.surfaces),
|
|
157
|
+
includeInLight: workflowBooleanInput(plan.inputs.includeInLight),
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return primitiveReportToWorkflowStepResult(
|
|
162
|
+
await addField({
|
|
163
|
+
app: workflowStringInput(plan.inputs.app),
|
|
164
|
+
module: workflowStringInput(plan.inputs.module),
|
|
165
|
+
field: workflowStringInput(plan.inputs.field),
|
|
166
|
+
type: workflowStringInput(plan.inputs.type),
|
|
167
|
+
defaultValue: workflowStringInput(plan.inputs.default),
|
|
168
|
+
surfaces: workflowStringArrayInput(plan.inputs.surfaces),
|
|
169
|
+
includeInLight: workflowBooleanInput(plan.inputs.includeInLight),
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
[workflowStepKey("add-field", "update-dictionary")]: inspect,
|
|
174
|
+
[workflowStepKey("add-field", "update-ui-surfaces")]: async (_step, plan) => addFieldUiSurfaceInspection(plan),
|
|
175
|
+
[workflowStepKey("add-enum-field", "update-constant")]: async (_step, plan) =>
|
|
176
|
+
primitiveReportToWorkflowStepResult(
|
|
177
|
+
await addEnumField({
|
|
178
|
+
app: workflowStringInput(plan.inputs.app),
|
|
179
|
+
module: workflowStringInput(plan.inputs.module),
|
|
180
|
+
field: workflowStringInput(plan.inputs.field),
|
|
181
|
+
values: workflowStringListInput(plan.inputs.values),
|
|
182
|
+
defaultValue: workflowStringInput(plan.inputs.default),
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
186
|
+
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const createWorkflowStepCommandResult = (
|
|
191
|
+
step: WorkflowStep,
|
|
192
|
+
command: string,
|
|
193
|
+
reason: string,
|
|
194
|
+
): WorkflowStepResult => ({
|
|
195
|
+
commands: [{ command, reason, stepId: step.id }],
|
|
196
|
+
nextActions: [{ command, reason }],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export class WorkflowExecutor {
|
|
200
|
+
constructor(private readonly registry: WorkflowStepRegistry) {}
|
|
201
|
+
|
|
202
|
+
async apply(plan: WorkflowPlan) {
|
|
203
|
+
const changedFiles: PrimitiveChangedFile[] = [];
|
|
204
|
+
const generatedFiles: PrimitiveGeneratedFile[] = [];
|
|
205
|
+
const recommendedValidationCommands: WorkflowApplyCommand[] = [];
|
|
206
|
+
const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
|
|
207
|
+
const recommendations = [...plan.recommendations];
|
|
208
|
+
const nextActions: PrimitiveNextAction[] = [];
|
|
209
|
+
|
|
210
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
211
|
+
return createWorkflowApplyReport({
|
|
212
|
+
workflow: plan.workflow,
|
|
213
|
+
mode: "apply",
|
|
214
|
+
changedFiles,
|
|
215
|
+
generatedFiles,
|
|
216
|
+
recommendedValidationCommands,
|
|
217
|
+
diagnostics,
|
|
218
|
+
recommendations,
|
|
219
|
+
nextActions,
|
|
220
|
+
plan,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
recommendedValidationCommands.push(...workflowCommandsForPlan(plan));
|
|
225
|
+
nextActions.push(...workflowCommandsForPlan(plan));
|
|
226
|
+
|
|
227
|
+
for (const step of plan.steps) {
|
|
228
|
+
const runner =
|
|
229
|
+
this.registry[workflowStepKey(plan.workflow, step.id)] ?? this.registry[step.tool] ?? this.registry[step.id];
|
|
230
|
+
if (!runner) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
severity: "error",
|
|
233
|
+
code: "workflow-step-unsupported",
|
|
234
|
+
message: `Workflow ${plan.workflow} step "${step.id}" is not supported by workflow apply yet.`,
|
|
235
|
+
});
|
|
236
|
+
nextActions.push({
|
|
237
|
+
command: `akan workflow explain ${plan.workflow}`,
|
|
238
|
+
reason: "Review the unsupported workflow step before retrying apply.",
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = await runner(step, plan);
|
|
244
|
+
if (!result) continue;
|
|
245
|
+
changedFiles.push(...(result.changedFiles ?? []));
|
|
246
|
+
generatedFiles.push(...(result.generatedFiles ?? []));
|
|
247
|
+
recommendedValidationCommands.push(...(result.commands ?? []));
|
|
248
|
+
diagnostics.push(...(result.diagnostics ?? []));
|
|
249
|
+
recommendations.push(...(result.recommendations ?? []));
|
|
250
|
+
nextActions.push(...(result.nextActions ?? []));
|
|
251
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return createWorkflowApplyReport({
|
|
255
|
+
workflow: plan.workflow,
|
|
256
|
+
mode: "apply",
|
|
257
|
+
changedFiles,
|
|
258
|
+
generatedFiles,
|
|
259
|
+
recommendedValidationCommands,
|
|
260
|
+
diagnostics,
|
|
261
|
+
recommendations,
|
|
262
|
+
nextActions,
|
|
263
|
+
plan,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|