@akanjs/devkit 2.3.9-rc.1 → 2.3.9-rc.10
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 +195 -14
- package/akanMcpContract.test.ts +37 -0
- package/akanMcpContract.ts +701 -0
- package/devkitUtils.test.ts +2 -2
- package/executors.ts +22 -17
- package/getModelFileData.ts +5 -2
- package/index.ts +1 -0
- package/package.json +2 -2
- package/workflow/artifacts.ts +571 -0
- package/workflow/executor.ts +590 -0
- package/workflow/index.ts +11 -1089
- package/workflow/moduleIndex.test.ts +296 -0
- package/workflow/moduleIndex.ts +504 -0
- package/workflow/plan.ts +429 -0
- package/workflow/primitive.ts +59 -0
- package/workflow/render.ts +335 -0
- package/workflow/rolloutGate.test.ts +109 -0
- package/workflow/rolloutGate.ts +141 -0
- package/workflow/source.test.ts +62 -0
- package/workflow/source.ts +864 -0
- package/workflow/types.ts +475 -0
- package/workflow/uiPolicy.ts +45 -0
- package/workflow/utils.ts +23 -0
package/workflow/index.ts
CHANGED
|
@@ -1,1089 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
export interface PrimitiveTargetInput {
|
|
13
|
-
app: string | null;
|
|
14
|
-
module: string | null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface AddFieldInput extends PrimitiveTargetInput {
|
|
18
|
-
field: string | null;
|
|
19
|
-
type: string | null;
|
|
20
|
-
defaultValue?: string | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface AddEnumFieldInput extends PrimitiveTargetInput {
|
|
24
|
-
field: string | null;
|
|
25
|
-
values: string | null;
|
|
26
|
-
defaultValue?: string | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface WorkflowInputSpec {
|
|
30
|
-
type: WorkflowInputType;
|
|
31
|
-
required?: boolean;
|
|
32
|
-
description: string;
|
|
33
|
-
allowedValues?: readonly string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface WorkflowStep {
|
|
37
|
-
id: string;
|
|
38
|
-
title: string;
|
|
39
|
-
tool: string;
|
|
40
|
-
description: string;
|
|
41
|
-
when?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface WorkflowPredictedChange {
|
|
45
|
-
target: string;
|
|
46
|
-
action: "inspect" | "create" | "modify" | "sync" | "validate";
|
|
47
|
-
reason: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface WorkflowValidation {
|
|
51
|
-
command: string;
|
|
52
|
-
reason: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface WorkflowSpec {
|
|
56
|
-
schemaVersion: 1;
|
|
57
|
-
name: string;
|
|
58
|
-
description: string;
|
|
59
|
-
whenToUse: string;
|
|
60
|
-
inputs: Record<string, WorkflowInputSpec>;
|
|
61
|
-
optionalSurfaces?: Record<string, WorkflowSurfaceMode>;
|
|
62
|
-
steps: readonly WorkflowStep[];
|
|
63
|
-
predictedChanges: readonly WorkflowPredictedChange[];
|
|
64
|
-
validation: readonly WorkflowValidation[];
|
|
65
|
-
completionCriteria: readonly string[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface WorkflowDiagnostic {
|
|
69
|
-
severity: "warning" | "error";
|
|
70
|
-
code: string;
|
|
71
|
-
message: string;
|
|
72
|
-
input?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface WorkflowPlan {
|
|
76
|
-
schemaVersion: 1;
|
|
77
|
-
workflow: string;
|
|
78
|
-
mode: "plan";
|
|
79
|
-
inputs: Record<string, WorkflowInputValue>;
|
|
80
|
-
optionalSurfaces: Record<string, WorkflowSurfaceMode>;
|
|
81
|
-
steps: readonly WorkflowStep[];
|
|
82
|
-
predictedChanges: readonly WorkflowPredictedChange[];
|
|
83
|
-
validation: readonly WorkflowValidation[];
|
|
84
|
-
diagnostics: WorkflowDiagnostic[];
|
|
85
|
-
requiresApproval: true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface WorkflowReport {
|
|
89
|
-
schemaVersion: 1;
|
|
90
|
-
workflow: string;
|
|
91
|
-
mode: "plan" | "dry-run" | "apply";
|
|
92
|
-
status: "passed" | "failed";
|
|
93
|
-
diagnostics: WorkflowDiagnostic[];
|
|
94
|
-
plan?: WorkflowPlan;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface WorkflowApplyCommand {
|
|
98
|
-
command: string;
|
|
99
|
-
reason: string;
|
|
100
|
-
stepId?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export type WorkflowRunSource =
|
|
104
|
-
| { type: "plan"; path: string }
|
|
105
|
-
| { type: "apply-report"; path: string; runId?: string }
|
|
106
|
-
| { type: "run-report"; runId: string };
|
|
107
|
-
|
|
108
|
-
export interface WorkflowValidationCommandResult {
|
|
109
|
-
command: string;
|
|
110
|
-
reason: string;
|
|
111
|
-
status: "passed" | "failed";
|
|
112
|
-
exitCode: number;
|
|
113
|
-
stdout?: string;
|
|
114
|
-
stderr?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface RepairAction {
|
|
118
|
-
command: string;
|
|
119
|
-
reason: string;
|
|
120
|
-
kind: "generated" | "format" | "imports" | "dictionary" | "module-shape";
|
|
121
|
-
safeToRun: boolean;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface PrimitiveChangedFile {
|
|
125
|
-
path: string;
|
|
126
|
-
action: "create" | "modify" | "remove";
|
|
127
|
-
reason: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface PrimitiveGeneratedFile {
|
|
131
|
-
path: string;
|
|
132
|
-
action: "sync";
|
|
133
|
-
reason: string;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface PrimitiveValidationCommand {
|
|
137
|
-
command: string;
|
|
138
|
-
reason: string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface PrimitiveNextAction {
|
|
142
|
-
command: string;
|
|
143
|
-
reason: string;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface PrimitiveWriteReport {
|
|
147
|
-
schemaVersion: 1;
|
|
148
|
-
command: string;
|
|
149
|
-
status: "passed" | "failed";
|
|
150
|
-
changedFiles: PrimitiveChangedFile[];
|
|
151
|
-
generatedFiles: PrimitiveGeneratedFile[];
|
|
152
|
-
validationCommands: PrimitiveValidationCommand[];
|
|
153
|
-
diagnostics: WorkflowDiagnostic[];
|
|
154
|
-
nextActions: PrimitiveNextAction[];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export type PrimitiveFileMap = Record<string, { filename: string; content: string }>;
|
|
158
|
-
|
|
159
|
-
export interface WorkflowApplyReport {
|
|
160
|
-
schemaVersion: 1;
|
|
161
|
-
workflow: string;
|
|
162
|
-
mode: "dry-run" | "apply";
|
|
163
|
-
status: "passed" | "failed";
|
|
164
|
-
changedFiles: PrimitiveChangedFile[];
|
|
165
|
-
generatedFiles: PrimitiveGeneratedFile[];
|
|
166
|
-
commands: WorkflowApplyCommand[];
|
|
167
|
-
diagnostics: WorkflowDiagnostic[];
|
|
168
|
-
nextActions: PrimitiveNextAction[];
|
|
169
|
-
plan: WorkflowPlan;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export interface WorkflowValidationRunReport {
|
|
173
|
-
schemaVersion: 1;
|
|
174
|
-
runId: string;
|
|
175
|
-
workflow: string;
|
|
176
|
-
mode: "validate";
|
|
177
|
-
source: WorkflowRunSource;
|
|
178
|
-
status: "passed" | "failed";
|
|
179
|
-
commands: WorkflowValidationCommandResult[];
|
|
180
|
-
diagnostics: WorkflowDiagnostic[];
|
|
181
|
-
repairActions: RepairAction[];
|
|
182
|
-
nextActions: PrimitiveNextAction[];
|
|
183
|
-
plan?: WorkflowPlan;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface RepairReport {
|
|
187
|
-
schemaVersion: 1;
|
|
188
|
-
command: string;
|
|
189
|
-
kind: RepairAction["kind"];
|
|
190
|
-
target: string | null;
|
|
191
|
-
status: "passed" | "failed";
|
|
192
|
-
diagnostics: WorkflowDiagnostic[];
|
|
193
|
-
repairActions: RepairAction[];
|
|
194
|
-
nextActions: PrimitiveNextAction[];
|
|
195
|
-
commands: WorkflowValidationCommandResult[];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export type WorkflowRunArtifact = WorkflowApplyReport | WorkflowValidationRunReport | RepairReport;
|
|
199
|
-
|
|
200
|
-
export interface WorkflowStepResult {
|
|
201
|
-
changedFiles?: PrimitiveChangedFile[];
|
|
202
|
-
generatedFiles?: PrimitiveGeneratedFile[];
|
|
203
|
-
commands?: WorkflowApplyCommand[];
|
|
204
|
-
diagnostics?: WorkflowDiagnostic[];
|
|
205
|
-
nextActions?: PrimitiveNextAction[];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export type WorkflowStepRunner = (step: WorkflowStep, plan: WorkflowPlan) => Promise<WorkflowStepResult | undefined>;
|
|
209
|
-
export type WorkflowStepRegistry = Record<string, WorkflowStepRunner>;
|
|
210
|
-
|
|
211
|
-
export interface WorkflowPrimitiveOperations {
|
|
212
|
-
workspace: Workspace;
|
|
213
|
-
createModule: (sys: Sys, module: string) => Promise<PrimitiveWriteReport>;
|
|
214
|
-
createScalar: (sys: Sys, scalar: string) => Promise<PrimitiveWriteReport>;
|
|
215
|
-
createUi: (input: PrimitiveTargetInput & { surface: UiSurface }) => Promise<PrimitiveWriteReport>;
|
|
216
|
-
addField: (input: AddFieldInput) => Promise<PrimitiveWriteReport>;
|
|
217
|
-
addEnumField: (input: AddEnumFieldInput) => Promise<PrimitiveWriteReport>;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export const jsonText = (value: unknown, { trailingNewline = true }: { trailingNewline?: boolean } = {}) =>
|
|
221
|
-
`${JSON.stringify(value, null, 2)}${trailingNewline ? "\n" : ""}`;
|
|
222
|
-
|
|
223
|
-
const surfaceModes = new Set<WorkflowSurfaceMode>(["infer", "include", "skip"]);
|
|
224
|
-
|
|
225
|
-
const parseStringList = (value: unknown) => {
|
|
226
|
-
if (Array.isArray(value)) {
|
|
227
|
-
const values = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
228
|
-
return values.length === value.length ? values : null;
|
|
229
|
-
}
|
|
230
|
-
if (typeof value !== "string") return null;
|
|
231
|
-
return value
|
|
232
|
-
.split(",")
|
|
233
|
-
.map((item) => item.trim())
|
|
234
|
-
.filter(Boolean);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const normalizeInputValue = (name: string, spec: WorkflowInputSpec, value: unknown): WorkflowInputValue | null => {
|
|
238
|
-
if (spec.type === "string") return typeof value === "string" && value.length > 0 ? value : null;
|
|
239
|
-
if (spec.type === "string-list") {
|
|
240
|
-
const values = parseStringList(value);
|
|
241
|
-
return values && values.length > 0 ? values : null;
|
|
242
|
-
}
|
|
243
|
-
if (typeof value === "string" && surfaceModes.has(value as WorkflowSurfaceMode)) return value as WorkflowSurfaceMode;
|
|
244
|
-
throw new Error(`Unsupported workflow input value for ${name}`);
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
export const listWorkflowSpecs = (specs: readonly WorkflowSpec[]) =>
|
|
248
|
-
[...specs].sort((a, b) => a.name.localeCompare(b.name));
|
|
249
|
-
|
|
250
|
-
export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
|
|
251
|
-
specs.find((spec) => spec.name === name) ?? null;
|
|
252
|
-
|
|
253
|
-
export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
|
|
254
|
-
Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
|
|
255
|
-
|
|
256
|
-
export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string, unknown>): WorkflowPlan => {
|
|
257
|
-
const inputs: Record<string, WorkflowInputValue> = {};
|
|
258
|
-
const diagnostics: WorkflowDiagnostic[] = [];
|
|
259
|
-
|
|
260
|
-
for (const [name, inputSpec] of Object.entries(spec.inputs)) {
|
|
261
|
-
const rawValue = rawInputs[name];
|
|
262
|
-
if (rawValue === undefined || rawValue === null || rawValue === "") {
|
|
263
|
-
if (inputSpec.required) {
|
|
264
|
-
diagnostics.push({
|
|
265
|
-
severity: "error",
|
|
266
|
-
code: "workflow-input-missing",
|
|
267
|
-
input: name,
|
|
268
|
-
message: `Workflow ${spec.name} requires input "${name}".`,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const value = normalizeInputValue(name, inputSpec, rawValue);
|
|
275
|
-
if (value === null) {
|
|
276
|
-
diagnostics.push({
|
|
277
|
-
severity: "error",
|
|
278
|
-
code: "workflow-input-invalid",
|
|
279
|
-
input: name,
|
|
280
|
-
message: `Workflow ${spec.name} input "${name}" must be ${inputSpec.type}.`,
|
|
281
|
-
});
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (inputSpec.allowedValues && typeof value === "string" && !inputSpec.allowedValues.includes(value)) {
|
|
285
|
-
diagnostics.push({
|
|
286
|
-
severity: "error",
|
|
287
|
-
code: "workflow-input-not-allowed",
|
|
288
|
-
input: name,
|
|
289
|
-
message: `Workflow ${spec.name} input "${name}" must be one of: ${inputSpec.allowedValues.join(", ")}.`,
|
|
290
|
-
});
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
inputs[name] = value;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
schemaVersion: 1,
|
|
298
|
-
workflow: spec.name,
|
|
299
|
-
mode: "plan",
|
|
300
|
-
inputs,
|
|
301
|
-
optionalSurfaces: spec.optionalSurfaces ?? {},
|
|
302
|
-
steps: spec.steps,
|
|
303
|
-
predictedChanges: spec.predictedChanges,
|
|
304
|
-
validation: spec.validation,
|
|
305
|
-
diagnostics,
|
|
306
|
-
requiresApproval: true,
|
|
307
|
-
};
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
export const renderWorkflowList = (specs: readonly WorkflowSpec[]) =>
|
|
311
|
-
[
|
|
312
|
-
"# Akan Workflows",
|
|
313
|
-
"",
|
|
314
|
-
...specs.flatMap((spec) => [`- \`${spec.name}\`: ${spec.description}`, ` - When: ${spec.whenToUse}`]),
|
|
315
|
-
"",
|
|
316
|
-
].join("\n");
|
|
317
|
-
|
|
318
|
-
export const renderWorkflowExplain = (spec: WorkflowSpec) =>
|
|
319
|
-
[
|
|
320
|
-
`# Workflow: ${spec.name}`,
|
|
321
|
-
"",
|
|
322
|
-
spec.description,
|
|
323
|
-
"",
|
|
324
|
-
"## When To Use",
|
|
325
|
-
spec.whenToUse,
|
|
326
|
-
"",
|
|
327
|
-
"## Inputs",
|
|
328
|
-
...Object.entries(spec.inputs).map(
|
|
329
|
-
([name, input]) =>
|
|
330
|
-
`- \`${name}\`${input.required ? " (required)" : ""}: ${input.description}${
|
|
331
|
-
input.allowedValues ? ` Allowed: ${input.allowedValues.join(", ")}.` : ""
|
|
332
|
-
}`,
|
|
333
|
-
),
|
|
334
|
-
"",
|
|
335
|
-
"## Optional Surfaces",
|
|
336
|
-
...Object.entries(spec.optionalSurfaces ?? {}).map(([name, mode]) => `- \`${name}\`: ${mode}`),
|
|
337
|
-
"",
|
|
338
|
-
"## Steps",
|
|
339
|
-
...spec.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
|
|
340
|
-
"",
|
|
341
|
-
"## Validation",
|
|
342
|
-
...spec.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
|
|
343
|
-
"",
|
|
344
|
-
].join("\n");
|
|
345
|
-
|
|
346
|
-
export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
347
|
-
[
|
|
348
|
-
`# Workflow Plan: ${plan.workflow}`,
|
|
349
|
-
"",
|
|
350
|
-
`- Mode: ${plan.mode}`,
|
|
351
|
-
`- Requires approval: ${plan.requiresApproval}`,
|
|
352
|
-
"",
|
|
353
|
-
"## Inputs",
|
|
354
|
-
...Object.entries(plan.inputs).map(
|
|
355
|
-
([name, value]) => `- \`${name}\`: ${Array.isArray(value) ? value.join(", ") : value}`,
|
|
356
|
-
),
|
|
357
|
-
"",
|
|
358
|
-
"## Optional Surfaces",
|
|
359
|
-
...Object.entries(plan.optionalSurfaces).map(([name, mode]) => `- \`${name}\`: ${mode}`),
|
|
360
|
-
"",
|
|
361
|
-
"## Steps",
|
|
362
|
-
...plan.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
|
|
363
|
-
"",
|
|
364
|
-
"## Predicted Changes",
|
|
365
|
-
...plan.predictedChanges.map((change) => `- \`${change.action}\` ${change.target}: ${change.reason}`),
|
|
366
|
-
"",
|
|
367
|
-
"## Validation",
|
|
368
|
-
...plan.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
|
|
369
|
-
"",
|
|
370
|
-
"## Diagnostics",
|
|
371
|
-
...(plan.diagnostics.length
|
|
372
|
-
? plan.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
373
|
-
: ["- none"]),
|
|
374
|
-
"",
|
|
375
|
-
].join("\n");
|
|
376
|
-
|
|
377
|
-
const workflowStatus = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
378
|
-
diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed";
|
|
379
|
-
|
|
380
|
-
const commandStatus = (commands: readonly WorkflowValidationCommandResult[]) =>
|
|
381
|
-
commands.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
382
|
-
|
|
383
|
-
const uniqueBy = <T>(values: readonly T[], key: (value: T) => string) => {
|
|
384
|
-
const seen = new Set<string>();
|
|
385
|
-
return values.filter((value) => {
|
|
386
|
-
const itemKey = key(value);
|
|
387
|
-
if (seen.has(itemKey)) return false;
|
|
388
|
-
seen.add(itemKey);
|
|
389
|
-
return true;
|
|
390
|
-
});
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
export const createWorkflowApplyReport = ({
|
|
394
|
-
workflow,
|
|
395
|
-
mode,
|
|
396
|
-
changedFiles = [],
|
|
397
|
-
generatedFiles = [],
|
|
398
|
-
commands = [],
|
|
399
|
-
diagnostics = [],
|
|
400
|
-
nextActions = [],
|
|
401
|
-
plan,
|
|
402
|
-
}: Omit<WorkflowApplyReport, "schemaVersion" | "status">): WorkflowApplyReport => ({
|
|
403
|
-
schemaVersion: 1,
|
|
404
|
-
workflow,
|
|
405
|
-
mode,
|
|
406
|
-
status: workflowStatus(diagnostics),
|
|
407
|
-
changedFiles: uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
|
|
408
|
-
generatedFiles: uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
|
|
409
|
-
commands: uniqueBy(commands, (command) => command.command),
|
|
410
|
-
diagnostics,
|
|
411
|
-
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
412
|
-
plan,
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
|
|
416
|
-
const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
|
|
417
|
-
return command
|
|
418
|
-
.replaceAll("<app-or-lib-or-pkg>", target)
|
|
419
|
-
.replaceAll("<app-or-lib>", target)
|
|
420
|
-
.replaceAll("<app-name>", target);
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
|
|
424
|
-
plan.validation.map((validation) => ({
|
|
425
|
-
command: resolveWorkflowCommand(validation.command, plan),
|
|
426
|
-
reason: validation.reason,
|
|
427
|
-
})) satisfies WorkflowApplyCommand[];
|
|
428
|
-
|
|
429
|
-
export const workflowRunsDir = ".akan/workflows/runs";
|
|
430
|
-
|
|
431
|
-
export const createWorkflowRunId = (prefix = "run") =>
|
|
432
|
-
`${prefix}-${new Date()
|
|
433
|
-
.toISOString()
|
|
434
|
-
.replace(/[-:.TZ]/g, "")
|
|
435
|
-
.slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
436
|
-
|
|
437
|
-
const getRunId = (artifact: WorkflowRunArtifact) => {
|
|
438
|
-
if ("runId" in artifact) return artifact.runId;
|
|
439
|
-
return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
|
|
443
|
-
|
|
444
|
-
export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
|
|
445
|
-
const runId = getRunId(artifact);
|
|
446
|
-
const artifactPath = workflowRunArtifactPath(runId);
|
|
447
|
-
await workspace.writeFile(artifactPath, jsonText(artifact));
|
|
448
|
-
return { runId, path: artifactPath };
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
|
|
452
|
-
const artifactPath = workflowRunArtifactPath(runId);
|
|
453
|
-
if (!(await workspace.exists(artifactPath))) throw new Error(`Workflow run artifact does not exist: ${artifactPath}`);
|
|
454
|
-
return (await workspace.readJson(artifactPath)) as WorkflowRunArtifact;
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
export type WorkflowValidationCommandExecutor = (
|
|
458
|
-
command: WorkflowApplyCommand,
|
|
459
|
-
) => Promise<WorkflowValidationCommandResult>;
|
|
460
|
-
|
|
461
|
-
export const createWorkflowValidationRunReport = async ({
|
|
462
|
-
runId = createWorkflowRunId("validation"),
|
|
463
|
-
workflow,
|
|
464
|
-
source,
|
|
465
|
-
plan,
|
|
466
|
-
commands,
|
|
467
|
-
execute,
|
|
468
|
-
diagnostics = [],
|
|
469
|
-
repairActions = [],
|
|
470
|
-
}: {
|
|
471
|
-
runId?: string;
|
|
472
|
-
workflow: string;
|
|
473
|
-
source: WorkflowRunSource;
|
|
474
|
-
plan?: WorkflowPlan;
|
|
475
|
-
commands: WorkflowApplyCommand[];
|
|
476
|
-
execute: WorkflowValidationCommandExecutor;
|
|
477
|
-
diagnostics?: WorkflowDiagnostic[];
|
|
478
|
-
repairActions?: RepairAction[];
|
|
479
|
-
}): Promise<WorkflowValidationRunReport> => {
|
|
480
|
-
const results: WorkflowValidationCommandResult[] = [];
|
|
481
|
-
for (const command of commands) {
|
|
482
|
-
results.push(await execute(command));
|
|
483
|
-
}
|
|
484
|
-
const commandDiagnostics = results.flatMap((result) =>
|
|
485
|
-
result.status === "failed"
|
|
486
|
-
? [
|
|
487
|
-
{
|
|
488
|
-
severity: "error" as const,
|
|
489
|
-
code: "workflow-validation-command-failed",
|
|
490
|
-
message: `Validation command failed: ${result.command}`,
|
|
491
|
-
},
|
|
492
|
-
]
|
|
493
|
-
: [],
|
|
494
|
-
);
|
|
495
|
-
return {
|
|
496
|
-
schemaVersion: 1,
|
|
497
|
-
runId,
|
|
498
|
-
workflow,
|
|
499
|
-
mode: "validate",
|
|
500
|
-
source,
|
|
501
|
-
status: workflowStatus([...diagnostics, ...commandDiagnostics]) === "failed" ? "failed" : commandStatus(results),
|
|
502
|
-
commands: results,
|
|
503
|
-
diagnostics: [...diagnostics, ...commandDiagnostics],
|
|
504
|
-
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
505
|
-
nextActions: results
|
|
506
|
-
.filter((result) => result.status === "failed")
|
|
507
|
-
.map((result) => ({ command: result.command, reason: "Re-run this validation command after repair." })),
|
|
508
|
-
plan,
|
|
509
|
-
};
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
513
|
-
const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
514
|
-
if (change.action !== "create" && change.action !== "modify") return [];
|
|
515
|
-
return [
|
|
516
|
-
{
|
|
517
|
-
path: change.target,
|
|
518
|
-
action: change.action,
|
|
519
|
-
reason: change.reason,
|
|
520
|
-
},
|
|
521
|
-
];
|
|
522
|
-
});
|
|
523
|
-
const generatedFiles: PrimitiveGeneratedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
524
|
-
if (change.action !== "sync") return [];
|
|
525
|
-
return [
|
|
526
|
-
{
|
|
527
|
-
path: change.target,
|
|
528
|
-
action: "sync",
|
|
529
|
-
reason: change.reason,
|
|
530
|
-
},
|
|
531
|
-
];
|
|
532
|
-
});
|
|
533
|
-
const diagnostics = [...plan.diagnostics];
|
|
534
|
-
return createWorkflowApplyReport({
|
|
535
|
-
workflow: plan.workflow,
|
|
536
|
-
mode: "dry-run",
|
|
537
|
-
changedFiles,
|
|
538
|
-
generatedFiles,
|
|
539
|
-
commands: workflowCommandsForPlan(plan),
|
|
540
|
-
diagnostics,
|
|
541
|
-
nextActions: workflowCommandsForPlan(plan),
|
|
542
|
-
plan,
|
|
543
|
-
});
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
export const workflowStepKey = (workflow: string, stepId: string) => `${workflow}:${stepId}`;
|
|
547
|
-
|
|
548
|
-
export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport): WorkflowStepResult => ({
|
|
549
|
-
changedFiles: report.changedFiles,
|
|
550
|
-
generatedFiles: report.generatedFiles,
|
|
551
|
-
commands: report.validationCommands,
|
|
552
|
-
diagnostics: report.diagnostics,
|
|
553
|
-
nextActions: report.nextActions,
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
const workflowStringInput = (value: WorkflowInputValue | undefined) => (typeof value === "string" ? value : null);
|
|
557
|
-
const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
|
|
558
|
-
Array.isArray(value) ? value.join(",") : null;
|
|
559
|
-
|
|
560
|
-
const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
561
|
-
if (!target) return null;
|
|
562
|
-
const [apps, libs] = await workspace.getSyss();
|
|
563
|
-
if (apps.includes(target)) return AppExecutor.from(workspace, target);
|
|
564
|
-
if (libs.includes(target)) return LibExecutor.from(workspace, target);
|
|
565
|
-
return null;
|
|
566
|
-
};
|
|
567
|
-
|
|
568
|
-
const targetMissing = (input = "app"): WorkflowDiagnostic => ({
|
|
569
|
-
severity: "error",
|
|
570
|
-
code: "workflow-target-missing",
|
|
571
|
-
input,
|
|
572
|
-
message: "Workflow target app or library was not found.",
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
const inputMissing = (input: string): WorkflowDiagnostic => ({
|
|
576
|
-
severity: "error",
|
|
577
|
-
code: "workflow-input-missing",
|
|
578
|
-
input,
|
|
579
|
-
message: `Workflow input "${input}" is required for apply.`,
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
const unsupportedInput = (input: string, message: string): WorkflowDiagnostic => ({
|
|
583
|
-
severity: "error",
|
|
584
|
-
code: "workflow-input-unsupported",
|
|
585
|
-
input,
|
|
586
|
-
message,
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
export const createWorkflowStepRegistry = ({
|
|
590
|
-
workspace,
|
|
591
|
-
createModule,
|
|
592
|
-
createScalar,
|
|
593
|
-
createUi,
|
|
594
|
-
addField,
|
|
595
|
-
addEnumField,
|
|
596
|
-
}: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
|
|
597
|
-
const inspect = async () => undefined;
|
|
598
|
-
const commandOnly = async () => undefined;
|
|
599
|
-
|
|
600
|
-
return {
|
|
601
|
-
inspectSystem: inspect,
|
|
602
|
-
inspectModule: inspect,
|
|
603
|
-
syncTarget: commandOnly,
|
|
604
|
-
lintTarget: commandOnly,
|
|
605
|
-
[workflowStepKey("create-module", "create-module")]: async (_step, plan) => {
|
|
606
|
-
const app = workflowStringInput(plan.inputs.app);
|
|
607
|
-
const module = workflowStringInput(plan.inputs.module);
|
|
608
|
-
const sys = await resolveWorkflowSys(workspace, app);
|
|
609
|
-
if (!sys || !module) return { diagnostics: [!sys ? targetMissing() : inputMissing("module")] };
|
|
610
|
-
return primitiveReportToWorkflowStepResult(await createModule(sys, module));
|
|
611
|
-
},
|
|
612
|
-
[workflowStepKey("create-scalar", "create-scalar")]: async (_step, plan) => {
|
|
613
|
-
const app = workflowStringInput(plan.inputs.app);
|
|
614
|
-
const scalar = workflowStringInput(plan.inputs.scalar);
|
|
615
|
-
const sys = await resolveWorkflowSys(workspace, app);
|
|
616
|
-
if (!sys || !scalar) return { diagnostics: [!sys ? targetMissing() : inputMissing("scalar")] };
|
|
617
|
-
return primitiveReportToWorkflowStepResult(await createScalar(sys, scalar));
|
|
618
|
-
},
|
|
619
|
-
[workflowStepKey("create-ui", "create-ui")]: async (_step, plan) => {
|
|
620
|
-
const surface = workflowStringInput(plan.inputs.surface);
|
|
621
|
-
if (surface !== "view" && surface !== "unit" && surface !== "template") {
|
|
622
|
-
return {
|
|
623
|
-
diagnostics: [
|
|
624
|
-
unsupportedInput("surface", "Workflow apply currently supports create-ui surfaces: view, unit, template."),
|
|
625
|
-
],
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
return primitiveReportToWorkflowStepResult(
|
|
629
|
-
await createUi({
|
|
630
|
-
app: workflowStringInput(plan.inputs.app),
|
|
631
|
-
module: workflowStringInput(plan.inputs.module),
|
|
632
|
-
surface,
|
|
633
|
-
}),
|
|
634
|
-
);
|
|
635
|
-
},
|
|
636
|
-
[workflowStepKey("add-field", "update-constant")]: async (_step, plan) => {
|
|
637
|
-
if (workflowStringInput(plan.inputs.type)?.toLowerCase() === "enum") {
|
|
638
|
-
return primitiveReportToWorkflowStepResult(
|
|
639
|
-
await addEnumField({
|
|
640
|
-
app: workflowStringInput(plan.inputs.app),
|
|
641
|
-
module: workflowStringInput(plan.inputs.module),
|
|
642
|
-
field: workflowStringInput(plan.inputs.field),
|
|
643
|
-
values: workflowStringListInput(plan.inputs.values),
|
|
644
|
-
defaultValue: workflowStringInput(plan.inputs.default),
|
|
645
|
-
}),
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
return primitiveReportToWorkflowStepResult(
|
|
649
|
-
await addField({
|
|
650
|
-
app: workflowStringInput(plan.inputs.app),
|
|
651
|
-
module: workflowStringInput(plan.inputs.module),
|
|
652
|
-
field: workflowStringInput(plan.inputs.field),
|
|
653
|
-
type: workflowStringInput(plan.inputs.type),
|
|
654
|
-
defaultValue: workflowStringInput(plan.inputs.default),
|
|
655
|
-
}),
|
|
656
|
-
);
|
|
657
|
-
},
|
|
658
|
-
[workflowStepKey("add-field", "update-dictionary")]: inspect,
|
|
659
|
-
[workflowStepKey("add-field", "update-ui-surfaces")]: inspect,
|
|
660
|
-
[workflowStepKey("add-enum-field", "update-constant")]: async (_step, plan) =>
|
|
661
|
-
primitiveReportToWorkflowStepResult(
|
|
662
|
-
await addEnumField({
|
|
663
|
-
app: workflowStringInput(plan.inputs.app),
|
|
664
|
-
module: workflowStringInput(plan.inputs.module),
|
|
665
|
-
field: workflowStringInput(plan.inputs.field),
|
|
666
|
-
values: workflowStringListInput(plan.inputs.values),
|
|
667
|
-
defaultValue: workflowStringInput(plan.inputs.default),
|
|
668
|
-
}),
|
|
669
|
-
),
|
|
670
|
-
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
671
|
-
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
672
|
-
};
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
export const createWorkflowStepCommandResult = (
|
|
676
|
-
step: WorkflowStep,
|
|
677
|
-
command: string,
|
|
678
|
-
reason: string,
|
|
679
|
-
): WorkflowStepResult => ({
|
|
680
|
-
commands: [{ command, reason, stepId: step.id }],
|
|
681
|
-
nextActions: [{ command, reason }],
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
|
|
685
|
-
[
|
|
686
|
-
`# Workflow Apply: ${report.workflow}`,
|
|
687
|
-
"",
|
|
688
|
-
`- Mode: ${report.mode}`,
|
|
689
|
-
`- Status: ${report.status}`,
|
|
690
|
-
"",
|
|
691
|
-
"## Changed Files",
|
|
692
|
-
...(report.changedFiles.length
|
|
693
|
-
? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
694
|
-
: ["- none"]),
|
|
695
|
-
"",
|
|
696
|
-
"## Generated Files",
|
|
697
|
-
...(report.generatedFiles.length
|
|
698
|
-
? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
699
|
-
: ["- none"]),
|
|
700
|
-
"",
|
|
701
|
-
"## Commands",
|
|
702
|
-
...(report.commands.length
|
|
703
|
-
? report.commands.map((command) => `- \`${command.command}\`: ${command.reason}`)
|
|
704
|
-
: ["- none"]),
|
|
705
|
-
"",
|
|
706
|
-
"## Diagnostics",
|
|
707
|
-
...(report.diagnostics.length
|
|
708
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
709
|
-
: ["- none"]),
|
|
710
|
-
"",
|
|
711
|
-
"## Next Actions",
|
|
712
|
-
...(report.nextActions.length
|
|
713
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
714
|
-
: ["- none"]),
|
|
715
|
-
"",
|
|
716
|
-
].join("\n");
|
|
717
|
-
|
|
718
|
-
export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
|
|
719
|
-
format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
|
|
720
|
-
|
|
721
|
-
export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
|
|
722
|
-
[
|
|
723
|
-
`# Workflow Validation: ${report.workflow}`,
|
|
724
|
-
"",
|
|
725
|
-
`- Run: ${report.runId}`,
|
|
726
|
-
`- Status: ${report.status}`,
|
|
727
|
-
"",
|
|
728
|
-
"## Commands",
|
|
729
|
-
...(report.commands.length
|
|
730
|
-
? report.commands.map((command) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
|
|
731
|
-
: ["- none"]),
|
|
732
|
-
"",
|
|
733
|
-
"## Diagnostics",
|
|
734
|
-
...(report.diagnostics.length
|
|
735
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
736
|
-
: ["- none"]),
|
|
737
|
-
"",
|
|
738
|
-
"## Repair Actions",
|
|
739
|
-
...(report.repairActions.length
|
|
740
|
-
? report.repairActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
741
|
-
: ["- none"]),
|
|
742
|
-
"",
|
|
743
|
-
"## Next Actions",
|
|
744
|
-
...(report.nextActions.length
|
|
745
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
746
|
-
: ["- none"]),
|
|
747
|
-
"",
|
|
748
|
-
].join("\n");
|
|
749
|
-
|
|
750
|
-
export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
|
|
751
|
-
format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
|
|
752
|
-
|
|
753
|
-
export const renderWorkflowRunArtifact = (artifact: WorkflowRunArtifact, format: WorkflowFormat = "markdown") => {
|
|
754
|
-
if ("kind" in artifact) return renderRepairReport(artifact, format);
|
|
755
|
-
if ("mode" in artifact && artifact.mode === "validate") return renderWorkflowValidation(artifact, format);
|
|
756
|
-
if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
|
|
757
|
-
return renderWorkflowApply(artifact, format);
|
|
758
|
-
}
|
|
759
|
-
return jsonText(artifact);
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
export const createRepairReport = ({
|
|
763
|
-
command,
|
|
764
|
-
kind,
|
|
765
|
-
target = null,
|
|
766
|
-
diagnostics = [],
|
|
767
|
-
repairActions = [],
|
|
768
|
-
nextActions = [],
|
|
769
|
-
commands = [],
|
|
770
|
-
}: Omit<
|
|
771
|
-
RepairReport,
|
|
772
|
-
"schemaVersion" | "status" | "target" | "diagnostics" | "repairActions" | "nextActions" | "commands"
|
|
773
|
-
> &
|
|
774
|
-
Partial<
|
|
775
|
-
Pick<RepairReport, "target" | "diagnostics" | "repairActions" | "nextActions" | "commands">
|
|
776
|
-
>): RepairReport => ({
|
|
777
|
-
schemaVersion: 1,
|
|
778
|
-
command,
|
|
779
|
-
kind,
|
|
780
|
-
target,
|
|
781
|
-
status: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed" ? "failed" : "passed",
|
|
782
|
-
diagnostics,
|
|
783
|
-
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
784
|
-
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
785
|
-
commands,
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
export const renderRepairReportMarkdown = (report: RepairReport) =>
|
|
789
|
-
[
|
|
790
|
-
`# Akan Repair: ${report.kind}`,
|
|
791
|
-
"",
|
|
792
|
-
`- Status: ${report.status}`,
|
|
793
|
-
`- Target: ${report.target ?? "none"}`,
|
|
794
|
-
"",
|
|
795
|
-
"## Commands",
|
|
796
|
-
...(report.commands.length
|
|
797
|
-
? report.commands.map((command) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
|
|
798
|
-
: ["- none"]),
|
|
799
|
-
"",
|
|
800
|
-
"## Diagnostics",
|
|
801
|
-
...(report.diagnostics.length
|
|
802
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
803
|
-
: ["- none"]),
|
|
804
|
-
"",
|
|
805
|
-
"## Next Actions",
|
|
806
|
-
...(report.nextActions.length
|
|
807
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
808
|
-
: ["- none"]),
|
|
809
|
-
"",
|
|
810
|
-
].join("\n");
|
|
811
|
-
|
|
812
|
-
export const renderRepairReport = (report: RepairReport, format: WorkflowFormat = "markdown") =>
|
|
813
|
-
format === "json" ? jsonText(report) : renderRepairReportMarkdown(report);
|
|
814
|
-
|
|
815
|
-
export class WorkflowExecutor {
|
|
816
|
-
constructor(private readonly registry: WorkflowStepRegistry) {}
|
|
817
|
-
|
|
818
|
-
async apply(plan: WorkflowPlan): Promise<WorkflowApplyReport> {
|
|
819
|
-
const changedFiles: PrimitiveChangedFile[] = [];
|
|
820
|
-
const generatedFiles: PrimitiveGeneratedFile[] = [];
|
|
821
|
-
const commands: WorkflowApplyCommand[] = [];
|
|
822
|
-
const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
|
|
823
|
-
const nextActions: PrimitiveNextAction[] = [];
|
|
824
|
-
|
|
825
|
-
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
826
|
-
return createWorkflowApplyReport({
|
|
827
|
-
workflow: plan.workflow,
|
|
828
|
-
mode: "apply",
|
|
829
|
-
changedFiles,
|
|
830
|
-
generatedFiles,
|
|
831
|
-
commands,
|
|
832
|
-
diagnostics,
|
|
833
|
-
nextActions,
|
|
834
|
-
plan,
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
commands.push(...workflowCommandsForPlan(plan));
|
|
839
|
-
nextActions.push(...workflowCommandsForPlan(plan));
|
|
840
|
-
|
|
841
|
-
for (const step of plan.steps) {
|
|
842
|
-
const runner =
|
|
843
|
-
this.registry[workflowStepKey(plan.workflow, step.id)] ?? this.registry[step.tool] ?? this.registry[step.id];
|
|
844
|
-
if (!runner) {
|
|
845
|
-
diagnostics.push({
|
|
846
|
-
severity: "error",
|
|
847
|
-
code: "workflow-step-unsupported",
|
|
848
|
-
message: `Workflow ${plan.workflow} step "${step.id}" is not supported by workflow apply yet.`,
|
|
849
|
-
});
|
|
850
|
-
nextActions.push({
|
|
851
|
-
command: `akan workflow explain ${plan.workflow}`,
|
|
852
|
-
reason: "Review the unsupported workflow step before retrying apply.",
|
|
853
|
-
});
|
|
854
|
-
break;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const result = await runner(step, plan);
|
|
858
|
-
if (!result) continue;
|
|
859
|
-
changedFiles.push(...(result.changedFiles ?? []));
|
|
860
|
-
generatedFiles.push(...(result.generatedFiles ?? []));
|
|
861
|
-
commands.push(...(result.commands ?? []));
|
|
862
|
-
diagnostics.push(...(result.diagnostics ?? []));
|
|
863
|
-
nextActions.push(...(result.nextActions ?? []));
|
|
864
|
-
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return createWorkflowApplyReport({
|
|
868
|
-
workflow: plan.workflow,
|
|
869
|
-
mode: "apply",
|
|
870
|
-
changedFiles,
|
|
871
|
-
generatedFiles,
|
|
872
|
-
commands,
|
|
873
|
-
diagnostics,
|
|
874
|
-
nextActions,
|
|
875
|
-
plan,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
export const createPrimitiveWriteReport = ({
|
|
881
|
-
command,
|
|
882
|
-
status,
|
|
883
|
-
changedFiles = [],
|
|
884
|
-
generatedFiles = [],
|
|
885
|
-
validationCommands = [],
|
|
886
|
-
diagnostics = [],
|
|
887
|
-
nextActions = [],
|
|
888
|
-
}: Omit<PrimitiveWriteReport, "schemaVersion" | "status"> & {
|
|
889
|
-
status?: PrimitiveWriteReport["status"];
|
|
890
|
-
}): PrimitiveWriteReport => ({
|
|
891
|
-
schemaVersion: 1,
|
|
892
|
-
command,
|
|
893
|
-
status: status ?? (diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed"),
|
|
894
|
-
changedFiles,
|
|
895
|
-
generatedFiles,
|
|
896
|
-
validationCommands,
|
|
897
|
-
diagnostics,
|
|
898
|
-
nextActions,
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
export const renderPrimitiveWriteReport = (report: PrimitiveWriteReport) =>
|
|
902
|
-
[
|
|
903
|
-
`# Primitive Write: ${report.command}`,
|
|
904
|
-
"",
|
|
905
|
-
`- Status: ${report.status}`,
|
|
906
|
-
"",
|
|
907
|
-
"## Changed Files",
|
|
908
|
-
...(report.changedFiles.length
|
|
909
|
-
? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
910
|
-
: ["- none"]),
|
|
911
|
-
"",
|
|
912
|
-
"## Generated Files",
|
|
913
|
-
...(report.generatedFiles.length
|
|
914
|
-
? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
915
|
-
: ["- none"]),
|
|
916
|
-
"",
|
|
917
|
-
"## Validation Commands",
|
|
918
|
-
...(report.validationCommands.length
|
|
919
|
-
? report.validationCommands.map((validation) => `- \`${validation.command}\`: ${validation.reason}`)
|
|
920
|
-
: ["- none"]),
|
|
921
|
-
"",
|
|
922
|
-
"## Diagnostics",
|
|
923
|
-
...(report.diagnostics.length
|
|
924
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
925
|
-
: ["- none"]),
|
|
926
|
-
"",
|
|
927
|
-
"## Next Actions",
|
|
928
|
-
...(report.nextActions.length
|
|
929
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
930
|
-
: ["- none"]),
|
|
931
|
-
"",
|
|
932
|
-
].join("\n");
|
|
933
|
-
|
|
934
|
-
export const renderPrimitiveReport = (report: PrimitiveWriteReport, format: PrimitiveFormat = "markdown") =>
|
|
935
|
-
format === "json" ? jsonText(report) : renderPrimitiveWriteReport(report);
|
|
936
|
-
|
|
937
|
-
export const getSysRoot = (sys: Sys) => `${sys.type}s/${sys.name}`;
|
|
938
|
-
|
|
939
|
-
export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile["action"], reason: string) => ({
|
|
940
|
-
path: `${getSysRoot(sys)}/${path}`,
|
|
941
|
-
action,
|
|
942
|
-
reason,
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
|
|
946
|
-
[
|
|
947
|
-
{ path: `${getSysRoot(sys)}/lib/cnst.ts`, action: "sync", reason },
|
|
948
|
-
{ path: `${getSysRoot(sys)}/lib/dict.ts`, action: "sync", reason },
|
|
949
|
-
{ path: `${getSysRoot(sys)}/lib/option.ts`, action: "sync", reason },
|
|
950
|
-
{ path: `${getSysRoot(sys)}/lib/index.ts`, action: "sync", reason },
|
|
951
|
-
] satisfies PrimitiveGeneratedFile[];
|
|
952
|
-
|
|
953
|
-
export const validationCommandsForTarget = (target: string) =>
|
|
954
|
-
[
|
|
955
|
-
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files from source conventions." },
|
|
956
|
-
{ command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
|
|
957
|
-
] satisfies PrimitiveValidationCommand[];
|
|
958
|
-
|
|
959
|
-
export const nextActionsForTarget = (target: string) =>
|
|
960
|
-
[
|
|
961
|
-
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files after source changes." },
|
|
962
|
-
{ command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
|
|
963
|
-
] satisfies PrimitiveNextAction[];
|
|
964
|
-
|
|
965
|
-
export const createPassedPrimitiveReport = ({
|
|
966
|
-
command,
|
|
967
|
-
changedFiles,
|
|
968
|
-
generatedFiles,
|
|
969
|
-
target,
|
|
970
|
-
nextActions,
|
|
971
|
-
}: {
|
|
972
|
-
command: string;
|
|
973
|
-
changedFiles: PrimitiveChangedFile[];
|
|
974
|
-
generatedFiles?: PrimitiveGeneratedFile[];
|
|
975
|
-
target: string;
|
|
976
|
-
nextActions?: PrimitiveNextAction[];
|
|
977
|
-
}) =>
|
|
978
|
-
createPrimitiveWriteReport({
|
|
979
|
-
command,
|
|
980
|
-
changedFiles,
|
|
981
|
-
generatedFiles: generatedFiles ?? [],
|
|
982
|
-
validationCommands: validationCommandsForTarget(target),
|
|
983
|
-
diagnostics: [],
|
|
984
|
-
nextActions: nextActions ?? nextActionsForTarget(target),
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
export const scalarChangedFiles = (sys: Sys, scalarName: string, files: PrimitiveFileMap) =>
|
|
988
|
-
Object.values(files).map((file) =>
|
|
989
|
-
sourceFile(sys, `lib/__scalar/${scalarName}/${file.filename}`, "create", "Scalar source file was created."),
|
|
990
|
-
);
|
|
991
|
-
|
|
992
|
-
export const titleize = (value: string) =>
|
|
993
|
-
value
|
|
994
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
995
|
-
.replace(/[-_]+/g, " ")
|
|
996
|
-
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
997
|
-
|
|
998
|
-
export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
|
|
999
|
-
|
|
1000
|
-
export const compactDiagnostics = (diagnostics: Array<WorkflowDiagnostic | false | null | undefined>) =>
|
|
1001
|
-
diagnostics.filter((diagnostic): diagnostic is WorkflowDiagnostic => !!diagnostic);
|
|
1002
|
-
|
|
1003
|
-
export const fieldExpression = (typeName: string, defaultValue?: string | null) => {
|
|
1004
|
-
const normalizedTypes: Record<string, string> = {
|
|
1005
|
-
string: "String",
|
|
1006
|
-
number: "Number",
|
|
1007
|
-
boolean: "Boolean",
|
|
1008
|
-
date: "Date",
|
|
1009
|
-
};
|
|
1010
|
-
const typeExpression = normalizedTypes[typeName.toLowerCase()] ?? typeName;
|
|
1011
|
-
const defaultOption = defaultValue ? `, { default: ${JSON.stringify(defaultValue)} }` : "";
|
|
1012
|
-
return `field(${typeExpression}${defaultOption})`;
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
export const insertIntoObject = (content: string, className: string, line: string) => {
|
|
1016
|
-
const classIndex = content.indexOf(`export class ${className} extends via`);
|
|
1017
|
-
if (classIndex < 0) return null;
|
|
1018
|
-
const objectEndIndex = content.indexOf("}))", classIndex);
|
|
1019
|
-
if (objectEndIndex < 0) return null;
|
|
1020
|
-
const prefix = content.slice(0, objectEndIndex);
|
|
1021
|
-
const suffix = content.slice(objectEndIndex);
|
|
1022
|
-
const insertion = prefix.endsWith("\n") ? ` ${line}\n` : `\n ${line}\n`;
|
|
1023
|
-
return `${prefix}${insertion}${suffix}`;
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
|
-
export const ensureEnumImport = (content: string) => {
|
|
1027
|
-
if (content.includes("enumOf")) return content;
|
|
1028
|
-
const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
|
|
1029
|
-
if (baseImport) {
|
|
1030
|
-
const names = baseImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
1031
|
-
return content.replace(baseImport[0], `import { ${[...names, "enumOf"].sort().join(", ")} } from "akanjs/base";`);
|
|
1032
|
-
}
|
|
1033
|
-
return `import { enumOf } from "akanjs/base";\n${content}`;
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
export const insertEnumClass = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
1037
|
-
if (content.includes(`export class ${enumClassName} extends enumOf`)) return content;
|
|
1038
|
-
const enumClass = `export class ${enumClassName} extends enumOf("${enumName}", [\n${values
|
|
1039
|
-
.map((value) => ` ${JSON.stringify(value)},`)
|
|
1040
|
-
.join("\n")}\n] as const) {}\n\n`;
|
|
1041
|
-
const firstClassIndex = content.indexOf("export class ");
|
|
1042
|
-
if (firstClassIndex < 0) return `${content}\n${enumClass}`;
|
|
1043
|
-
return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
|
|
1044
|
-
};
|
|
1045
|
-
|
|
1046
|
-
export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
1047
|
-
if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
|
|
1048
|
-
const label = titleize(fieldName);
|
|
1049
|
-
const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
|
|
1050
|
-
if (modelIndex < 0) return null;
|
|
1051
|
-
const objectEndIndex = content.indexOf(" }))", modelIndex);
|
|
1052
|
-
if (objectEndIndex < 0) return null;
|
|
1053
|
-
return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label)}, ${JSON.stringify(
|
|
1054
|
-
label,
|
|
1055
|
-
)}]).desc([${JSON.stringify(label)}, ${JSON.stringify(label)}]),\n${content.slice(objectEndIndex)}`;
|
|
1056
|
-
};
|
|
1057
|
-
|
|
1058
|
-
export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
|
|
1059
|
-
if (new RegExp(`import type \\{[^}]*\\b${typeName}\\b[^}]*\\} from "${constantPath}";`).test(content)) return content;
|
|
1060
|
-
const importPattern = new RegExp(`import type \\{ ([^}]+) \\} from "${constantPath}";`);
|
|
1061
|
-
const existingImport = content.match(importPattern);
|
|
1062
|
-
if (existingImport !== null) {
|
|
1063
|
-
const names = existingImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
1064
|
-
return content.replace(
|
|
1065
|
-
existingImport[0],
|
|
1066
|
-
`import type { ${[...names, typeName].sort().join(", ")} } from "${constantPath}";`,
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
return `import type { ${typeName} } from "${constantPath}";\n${content}`;
|
|
1070
|
-
};
|
|
1071
|
-
|
|
1072
|
-
export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
1073
|
-
if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
|
|
1074
|
-
const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
|
|
1075
|
-
.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
|
|
1076
|
-
.join("\n")}\n }))\n`;
|
|
1077
|
-
const chainEndIndex = content.lastIndexOf(";");
|
|
1078
|
-
const insertBeforeIndex = [content.indexOf(".error("), content.indexOf(".translate("), chainEndIndex]
|
|
1079
|
-
.filter((index) => index >= 0)
|
|
1080
|
-
.sort((a, b) => a - b)[0];
|
|
1081
|
-
if (insertBeforeIndex === undefined) return null;
|
|
1082
|
-
return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
|
|
1083
|
-
};
|
|
1084
|
-
|
|
1085
|
-
export const parseValues = (value: string | null) =>
|
|
1086
|
-
value
|
|
1087
|
-
?.split(",")
|
|
1088
|
-
.map((item) => item.trim())
|
|
1089
|
-
.filter(Boolean) ?? [];
|
|
1
|
+
export * from "./artifacts";
|
|
2
|
+
export * from "./executor";
|
|
3
|
+
export * from "./moduleIndex";
|
|
4
|
+
export * from "./plan";
|
|
5
|
+
export * from "./primitive";
|
|
6
|
+
export * from "./render";
|
|
7
|
+
export * from "./rolloutGate";
|
|
8
|
+
export * from "./source";
|
|
9
|
+
export * from "./types";
|
|
10
|
+
export * from "./uiPolicy";
|
|
11
|
+
export * from "./utils";
|