@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
package/workflow/index.ts
CHANGED
|
@@ -1,1399 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export type PrimitiveFormat = "markdown" | "json";
|
|
11
|
-
export type UiSurface = "view" | "unit" | "template";
|
|
12
|
-
export type WorkflowValidationKind = "sync" | "lint" | "typecheck" | "doctor" | "custom";
|
|
13
|
-
export type WorkflowFailureScope = "workspace-config" | "environment" | "source-change" | "unknown";
|
|
14
|
-
|
|
15
|
-
export interface PrimitiveTargetInput {
|
|
16
|
-
app: string | null;
|
|
17
|
-
module: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface AddFieldInput extends PrimitiveTargetInput {
|
|
21
|
-
field: string | null;
|
|
22
|
-
type: string | null;
|
|
23
|
-
defaultValue?: string | null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface AddEnumFieldInput extends PrimitiveTargetInput {
|
|
27
|
-
field: string | null;
|
|
28
|
-
values: string | null;
|
|
29
|
-
defaultValue?: string | null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface WorkflowInputSpec {
|
|
33
|
-
type: WorkflowInputType;
|
|
34
|
-
required?: boolean;
|
|
35
|
-
description: string;
|
|
36
|
-
allowedValues?: readonly string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface WorkflowStep {
|
|
40
|
-
id: string;
|
|
41
|
-
title: string;
|
|
42
|
-
tool: string;
|
|
43
|
-
description: string;
|
|
44
|
-
when?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface WorkflowPredictedChange {
|
|
48
|
-
target: string;
|
|
49
|
-
action: "inspect" | "create" | "modify" | "sync" | "validate";
|
|
50
|
-
reason: string;
|
|
51
|
-
applyScope?: "auto" | "manual-review" | "generated-sync" | "validation";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface WorkflowValidation {
|
|
55
|
-
command: string;
|
|
56
|
-
reason: string;
|
|
57
|
-
kind?: WorkflowValidationKind;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface WorkflowSpec {
|
|
61
|
-
schemaVersion: 1;
|
|
62
|
-
name: string;
|
|
63
|
-
description: string;
|
|
64
|
-
whenToUse: string;
|
|
65
|
-
inputs: Record<string, WorkflowInputSpec>;
|
|
66
|
-
optionalSurfaces?: Record<string, WorkflowSurfaceMode>;
|
|
67
|
-
steps: readonly WorkflowStep[];
|
|
68
|
-
predictedChanges: readonly WorkflowPredictedChange[];
|
|
69
|
-
validation: readonly WorkflowValidation[];
|
|
70
|
-
completionCriteria: readonly string[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface WorkflowDiagnostic {
|
|
74
|
-
severity: "warning" | "error";
|
|
75
|
-
code: string;
|
|
76
|
-
message: string;
|
|
77
|
-
input?: string;
|
|
78
|
-
command?: string;
|
|
79
|
-
kind?: WorkflowValidationKind;
|
|
80
|
-
failureScope?: WorkflowFailureScope;
|
|
81
|
-
scope?: "baseline" | "workflow" | "unknown";
|
|
82
|
-
context?: {
|
|
83
|
-
workflow?: string;
|
|
84
|
-
planPath?: string;
|
|
85
|
-
runId?: string;
|
|
86
|
-
target?: string;
|
|
87
|
-
paths?: string[];
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface WorkflowRecommendation {
|
|
92
|
-
code: string;
|
|
93
|
-
message: string;
|
|
94
|
-
kind: "auto-apply" | "validation" | "manual-action" | "import" | "placement" | "ui-component";
|
|
95
|
-
target?: string;
|
|
96
|
-
action?: string;
|
|
97
|
-
confidence?: "high" | "medium" | "low";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface WorkflowPlan {
|
|
101
|
-
schemaVersion: 1;
|
|
102
|
-
workflow: string;
|
|
103
|
-
mode: "plan";
|
|
104
|
-
inputs: Record<string, WorkflowInputValue>;
|
|
105
|
-
optionalSurfaces: Record<string, WorkflowSurfaceMode>;
|
|
106
|
-
steps: readonly WorkflowStep[];
|
|
107
|
-
predictedChanges: readonly WorkflowPredictedChange[];
|
|
108
|
-
validation: readonly WorkflowValidation[];
|
|
109
|
-
diagnostics: WorkflowDiagnostic[];
|
|
110
|
-
recommendations: WorkflowRecommendation[];
|
|
111
|
-
requiresApproval: true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface WorkflowReport {
|
|
115
|
-
schemaVersion: 1;
|
|
116
|
-
workflow: string;
|
|
117
|
-
mode: "plan" | "dry-run" | "apply";
|
|
118
|
-
status: "passed" | "failed";
|
|
119
|
-
diagnostics: WorkflowDiagnostic[];
|
|
120
|
-
plan?: WorkflowPlan;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface WorkflowApplyCommand {
|
|
124
|
-
command: string;
|
|
125
|
-
reason: string;
|
|
126
|
-
stepId?: string;
|
|
127
|
-
kind?: WorkflowValidationKind;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export type WorkflowRunSource =
|
|
131
|
-
| { type: "plan"; path: string }
|
|
132
|
-
| { type: "apply-report"; path: string; runId?: string }
|
|
133
|
-
| { type: "run-report"; runId: string };
|
|
134
|
-
|
|
135
|
-
export interface WorkflowValidationCommandResult {
|
|
136
|
-
command: string;
|
|
137
|
-
reason: string;
|
|
138
|
-
kind?: WorkflowValidationKind;
|
|
139
|
-
status: "passed" | "failed";
|
|
140
|
-
exitCode: number;
|
|
141
|
-
failureScope?: WorkflowFailureScope;
|
|
142
|
-
stdout?: string;
|
|
143
|
-
stderr?: string;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface RepairAction {
|
|
147
|
-
command: string;
|
|
148
|
-
reason: string;
|
|
149
|
-
kind: "generated" | "format" | "imports" | "dictionary" | "module-shape";
|
|
150
|
-
safeToRun: boolean;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export interface PrimitiveChangedFile {
|
|
154
|
-
path: string;
|
|
155
|
-
action: "create" | "modify" | "remove";
|
|
156
|
-
reason: string;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export interface PrimitiveGeneratedFile {
|
|
160
|
-
path: string;
|
|
161
|
-
action: "sync";
|
|
162
|
-
reason: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface PrimitiveValidationCommand {
|
|
166
|
-
command: string;
|
|
167
|
-
reason: string;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface PrimitiveNextAction {
|
|
171
|
-
command: string;
|
|
172
|
-
reason: string;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export interface PrimitiveWriteReport {
|
|
176
|
-
schemaVersion: 1;
|
|
177
|
-
command: string;
|
|
178
|
-
status: "passed" | "failed";
|
|
179
|
-
changedFiles: PrimitiveChangedFile[];
|
|
180
|
-
generatedFiles: PrimitiveGeneratedFile[];
|
|
181
|
-
validationCommands: PrimitiveValidationCommand[];
|
|
182
|
-
diagnostics: WorkflowDiagnostic[];
|
|
183
|
-
nextActions: PrimitiveNextAction[];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export type PrimitiveFileMap = Record<string, { filename: string; content: string }>;
|
|
187
|
-
|
|
188
|
-
export interface WorkflowApplyReport {
|
|
189
|
-
schemaVersion: 1;
|
|
190
|
-
runId?: string;
|
|
191
|
-
applyReportPath?: string;
|
|
192
|
-
validationTarget?: string;
|
|
193
|
-
workflow: string;
|
|
194
|
-
mode: "dry-run" | "apply";
|
|
195
|
-
status: "passed" | "failed";
|
|
196
|
-
changedFiles: PrimitiveChangedFile[];
|
|
197
|
-
generatedFiles: PrimitiveGeneratedFile[];
|
|
198
|
-
appliedCommands: WorkflowApplyCommand[];
|
|
199
|
-
recommendedValidationCommands: WorkflowApplyCommand[];
|
|
200
|
-
commands: WorkflowApplyCommand[];
|
|
201
|
-
diagnostics: WorkflowDiagnostic[];
|
|
202
|
-
recommendations: WorkflowRecommendation[];
|
|
203
|
-
nextActions: PrimitiveNextAction[];
|
|
204
|
-
plan: WorkflowPlan;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export interface WorkflowValidationRunReport {
|
|
208
|
-
schemaVersion: 1;
|
|
209
|
-
runId: string;
|
|
210
|
-
workflow: string;
|
|
211
|
-
mode: "validate";
|
|
212
|
-
source: WorkflowRunSource;
|
|
213
|
-
status: "passed" | "failed";
|
|
214
|
-
commands: WorkflowValidationCommandResult[];
|
|
215
|
-
diagnostics: WorkflowDiagnostic[];
|
|
216
|
-
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
217
|
-
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
218
|
-
repairActions: RepairAction[];
|
|
219
|
-
nextActions: PrimitiveNextAction[];
|
|
220
|
-
plan?: WorkflowPlan;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface RepairReport {
|
|
224
|
-
schemaVersion: 1;
|
|
225
|
-
runId?: string;
|
|
226
|
-
repairReportPath?: string;
|
|
227
|
-
command: string;
|
|
228
|
-
kind: RepairAction["kind"];
|
|
229
|
-
target: string | null;
|
|
230
|
-
status: "passed" | "failed";
|
|
231
|
-
diagnostics: WorkflowDiagnostic[];
|
|
232
|
-
repairActions: RepairAction[];
|
|
233
|
-
nextActions: PrimitiveNextAction[];
|
|
234
|
-
commands: WorkflowValidationCommandResult[];
|
|
235
|
-
generatedFiles?: PrimitiveGeneratedFile[];
|
|
236
|
-
syncedAt?: string;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export interface GeneratedSyncState {
|
|
240
|
-
schemaVersion: 1;
|
|
241
|
-
target: string;
|
|
242
|
-
status: "passed" | "failed";
|
|
243
|
-
syncedAt: string;
|
|
244
|
-
command: string;
|
|
245
|
-
runId?: string;
|
|
246
|
-
generatedFiles: PrimitiveGeneratedFile[];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export type WorkflowRunArtifact = WorkflowApplyReport | WorkflowValidationRunReport | RepairReport;
|
|
250
|
-
|
|
251
|
-
export interface WorkflowStepResult {
|
|
252
|
-
changedFiles?: PrimitiveChangedFile[];
|
|
253
|
-
generatedFiles?: PrimitiveGeneratedFile[];
|
|
254
|
-
commands?: WorkflowApplyCommand[];
|
|
255
|
-
diagnostics?: WorkflowDiagnostic[];
|
|
256
|
-
recommendations?: WorkflowRecommendation[];
|
|
257
|
-
nextActions?: PrimitiveNextAction[];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export type WorkflowStepRunner = (step: WorkflowStep, plan: WorkflowPlan) => Promise<WorkflowStepResult | undefined>;
|
|
261
|
-
export type WorkflowStepRegistry = Record<string, WorkflowStepRunner>;
|
|
262
|
-
|
|
263
|
-
export interface WorkflowPrimitiveOperations {
|
|
264
|
-
workspace: Workspace;
|
|
265
|
-
createModule: (sys: Sys, module: string) => Promise<PrimitiveWriteReport>;
|
|
266
|
-
createScalar: (sys: Sys, scalar: string) => Promise<PrimitiveWriteReport>;
|
|
267
|
-
createUi: (input: PrimitiveTargetInput & { surface: UiSurface }) => Promise<PrimitiveWriteReport>;
|
|
268
|
-
addField: (input: AddFieldInput) => Promise<PrimitiveWriteReport>;
|
|
269
|
-
addEnumField: (input: AddEnumFieldInput) => Promise<PrimitiveWriteReport>;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export const jsonText = (value: unknown, { trailingNewline = true }: { trailingNewline?: boolean } = {}) =>
|
|
273
|
-
`${JSON.stringify(value, null, 2)}${trailingNewline ? "\n" : ""}`;
|
|
274
|
-
|
|
275
|
-
const surfaceModes = new Set<WorkflowSurfaceMode>(["infer", "include", "skip"]);
|
|
276
|
-
|
|
277
|
-
const parseStringList = (value: unknown) => {
|
|
278
|
-
if (Array.isArray(value)) {
|
|
279
|
-
const values = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
280
|
-
return values.length === value.length ? values : null;
|
|
281
|
-
}
|
|
282
|
-
if (typeof value !== "string") return null;
|
|
283
|
-
return value
|
|
284
|
-
.split(",")
|
|
285
|
-
.map((item) => item.trim())
|
|
286
|
-
.filter(Boolean);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const normalizeInputValue = (name: string, spec: WorkflowInputSpec, value: unknown): WorkflowInputValue | null => {
|
|
290
|
-
if (spec.type === "string") return typeof value === "string" && value.length > 0 ? value : null;
|
|
291
|
-
if (spec.type === "string-list") {
|
|
292
|
-
const values = parseStringList(value);
|
|
293
|
-
return values && values.length > 0 ? values : null;
|
|
294
|
-
}
|
|
295
|
-
if (typeof value === "string" && surfaceModes.has(value as WorkflowSurfaceMode)) return value as WorkflowSurfaceMode;
|
|
296
|
-
throw new Error(`Unsupported workflow input value for ${name}`);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
export const listWorkflowSpecs = (specs: readonly WorkflowSpec[]) =>
|
|
300
|
-
[...specs].sort((a, b) => a.name.localeCompare(b.name));
|
|
301
|
-
|
|
302
|
-
export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
|
|
303
|
-
specs.find((spec) => spec.name === name) ?? null;
|
|
304
|
-
|
|
305
|
-
export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
|
|
306
|
-
Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
|
|
307
|
-
|
|
308
|
-
const addFieldComponentForType = (typeName: string) => {
|
|
309
|
-
const normalizedType = normalizeFieldType(typeName);
|
|
310
|
-
if (normalizedType === "Boolean") return "Field.ToggleSelect";
|
|
311
|
-
if (normalizedType === "Date") return "Field.Date";
|
|
312
|
-
if (normalizedType === "Int" || normalizedType === "Float") return "Field.ToggleSelect or Field.Text";
|
|
313
|
-
if (typeName.toLowerCase() === "enum") return "Field.ToggleSelect";
|
|
314
|
-
return "Field.Text";
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
|
|
318
|
-
const app = typeof inputs.app === "string" ? inputs.app : "<app-or-lib>";
|
|
319
|
-
const module = typeof inputs.module === "string" ? inputs.module : "<module>";
|
|
320
|
-
const field = typeof inputs.field === "string" ? inputs.field : "<field>";
|
|
321
|
-
const typeName = typeof inputs.type === "string" ? inputs.type : null;
|
|
322
|
-
if (!typeName) return [];
|
|
323
|
-
|
|
324
|
-
const normalizedType = typeName.toLowerCase() === "enum" ? "enum" : normalizeFieldType(typeName);
|
|
325
|
-
const constantPath = `*/lib/${module}/${module}.constant.ts`;
|
|
326
|
-
const dictionaryPath = `*/lib/${module}/${module}.dictionary.ts`;
|
|
327
|
-
const templatePath = `*/lib/${module}/${capitalize(module)}.Template.tsx`;
|
|
328
|
-
return [
|
|
329
|
-
...(normalizedType === "Int" || normalizedType === "Float"
|
|
330
|
-
? [
|
|
331
|
-
{
|
|
332
|
-
code: "add-field-import",
|
|
333
|
-
kind: "import" as const,
|
|
334
|
-
target: constantPath,
|
|
335
|
-
confidence: "high" as const,
|
|
336
|
-
message: `Import ${normalizedType} from "akanjs/base" before writing field(${normalizedType}).`,
|
|
337
|
-
},
|
|
338
|
-
]
|
|
339
|
-
: []),
|
|
340
|
-
{
|
|
341
|
-
code: "add-field-placement-constant",
|
|
342
|
-
kind: "placement",
|
|
343
|
-
target: constantPath,
|
|
344
|
-
confidence: "high",
|
|
345
|
-
message: `Insert ${field}: field(${normalizedType}) in ${capitalize(module)}Input.`,
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
code: "add-field-placement-dictionary",
|
|
349
|
-
kind: "placement",
|
|
350
|
-
target: dictionaryPath,
|
|
351
|
-
confidence: "high",
|
|
352
|
-
message: `Add dictionary labels for ${module}.${field}.`,
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
code: "add-field-component",
|
|
356
|
-
kind: "ui-component",
|
|
357
|
-
target: templatePath,
|
|
358
|
-
confidence: normalizedType === "Int" || normalizedType === "Float" ? "medium" : "high",
|
|
359
|
-
message: `Recommended UI component for ${field} (${normalizedType}): ${addFieldComponentForType(typeName)}.`,
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
code: "add-field-ui-manual-review",
|
|
363
|
-
kind: "manual-action",
|
|
364
|
-
target: templatePath,
|
|
365
|
-
action: `Review ${app}:${module} Template/Unit/View/Store surfaces and add ${field} only where the existing pattern is clear.`,
|
|
366
|
-
confidence: "medium",
|
|
367
|
-
message: "UI surface edits are planned as manual-review unless a safe existing field-list pattern is detected.",
|
|
368
|
-
},
|
|
369
|
-
];
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
const createWorkflowPlanRecommendations = (
|
|
373
|
-
spec: WorkflowSpec,
|
|
374
|
-
inputs: Record<string, WorkflowInputValue>,
|
|
375
|
-
): WorkflowRecommendation[] => [
|
|
376
|
-
{
|
|
377
|
-
code: "workflow-apply-first",
|
|
378
|
-
kind: "auto-apply",
|
|
379
|
-
confidence: "high",
|
|
380
|
-
action: "Call apply_workflow with the MCP planPath before editing source files directly.",
|
|
381
|
-
message: `Apply the ${spec.name} plan through apply_workflow when MCP returns planPath or next.tool=apply_workflow.`,
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
code: "workflow-validate-apply-report",
|
|
385
|
-
kind: "validation",
|
|
386
|
-
confidence: "high",
|
|
387
|
-
action:
|
|
388
|
-
"After apply_workflow, call run_validation with validationTarget when present; otherwise use applyReportPath.",
|
|
389
|
-
message: "Validate the apply report artifact so diagnostics and recommendations follow the actual apply result.",
|
|
390
|
-
},
|
|
391
|
-
...(spec.name === "add-field" ? createAddFieldRecommendations(inputs) : []),
|
|
392
|
-
];
|
|
393
|
-
|
|
394
|
-
export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string, unknown>): WorkflowPlan => {
|
|
395
|
-
const inputs: Record<string, WorkflowInputValue> = {};
|
|
396
|
-
const diagnostics: WorkflowDiagnostic[] = [];
|
|
397
|
-
|
|
398
|
-
for (const [name, inputSpec] of Object.entries(spec.inputs)) {
|
|
399
|
-
const rawValue = rawInputs[name];
|
|
400
|
-
if (rawValue === undefined || rawValue === null || rawValue === "") {
|
|
401
|
-
if (inputSpec.required) {
|
|
402
|
-
diagnostics.push({
|
|
403
|
-
severity: "error",
|
|
404
|
-
code: "workflow-input-missing",
|
|
405
|
-
input: name,
|
|
406
|
-
message: `Workflow ${spec.name} requires input "${name}".`,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const value = normalizeInputValue(name, inputSpec, rawValue);
|
|
413
|
-
if (value === null) {
|
|
414
|
-
diagnostics.push({
|
|
415
|
-
severity: "error",
|
|
416
|
-
code: "workflow-input-invalid",
|
|
417
|
-
input: name,
|
|
418
|
-
message: `Workflow ${spec.name} input "${name}" must be ${inputSpec.type}.`,
|
|
419
|
-
});
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
if (inputSpec.allowedValues && typeof value === "string" && !inputSpec.allowedValues.includes(value)) {
|
|
423
|
-
diagnostics.push({
|
|
424
|
-
severity: "error",
|
|
425
|
-
code: "workflow-input-not-allowed",
|
|
426
|
-
input: name,
|
|
427
|
-
message: `Workflow ${spec.name} input "${name}" must be one of: ${inputSpec.allowedValues.join(", ")}.`,
|
|
428
|
-
});
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
inputs[name] = value;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const fieldType = inputs.type;
|
|
435
|
-
if (
|
|
436
|
-
spec.name === "add-field" &&
|
|
437
|
-
typeof fieldType === "string" &&
|
|
438
|
-
(fieldType.toLowerCase() === "number" || fieldType.toLowerCase() === "numeric")
|
|
439
|
-
) {
|
|
440
|
-
diagnostics.push({
|
|
441
|
-
severity: "error",
|
|
442
|
-
code: "primitive-field-type-unsupported",
|
|
443
|
-
input: "type",
|
|
444
|
-
message: `Field type "${fieldType}" is ambiguous in Akan. Use Int for integer fields or Float for decimal fields.`,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
schemaVersion: 1,
|
|
450
|
-
workflow: spec.name,
|
|
451
|
-
mode: "plan",
|
|
452
|
-
inputs,
|
|
453
|
-
optionalSurfaces: spec.optionalSurfaces ?? {},
|
|
454
|
-
steps: spec.steps,
|
|
455
|
-
predictedChanges: spec.predictedChanges,
|
|
456
|
-
validation: spec.validation,
|
|
457
|
-
diagnostics,
|
|
458
|
-
recommendations: createWorkflowPlanRecommendations(spec, inputs),
|
|
459
|
-
requiresApproval: true,
|
|
460
|
-
};
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
export const renderWorkflowList = (specs: readonly WorkflowSpec[]) =>
|
|
464
|
-
[
|
|
465
|
-
"# Akan Workflows",
|
|
466
|
-
"",
|
|
467
|
-
...specs.flatMap((spec) => [`- \`${spec.name}\`: ${spec.description}`, ` - When: ${spec.whenToUse}`]),
|
|
468
|
-
"",
|
|
469
|
-
].join("\n");
|
|
470
|
-
|
|
471
|
-
export const renderWorkflowExplain = (spec: WorkflowSpec) =>
|
|
472
|
-
[
|
|
473
|
-
`# Workflow: ${spec.name}`,
|
|
474
|
-
"",
|
|
475
|
-
spec.description,
|
|
476
|
-
"",
|
|
477
|
-
"## When To Use",
|
|
478
|
-
spec.whenToUse,
|
|
479
|
-
"",
|
|
480
|
-
"## Inputs",
|
|
481
|
-
...Object.entries(spec.inputs).map(
|
|
482
|
-
([name, input]) =>
|
|
483
|
-
`- \`${name}\`${input.required ? " (required)" : ""}: ${input.description}${
|
|
484
|
-
input.allowedValues ? ` Allowed: ${input.allowedValues.join(", ")}.` : ""
|
|
485
|
-
}`,
|
|
486
|
-
),
|
|
487
|
-
"",
|
|
488
|
-
"## Optional Surfaces",
|
|
489
|
-
...Object.entries(spec.optionalSurfaces ?? {}).map(([name, mode]) => `- \`${name}\`: ${mode}`),
|
|
490
|
-
"",
|
|
491
|
-
"## Steps",
|
|
492
|
-
...spec.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
|
|
493
|
-
"",
|
|
494
|
-
"## Validation",
|
|
495
|
-
...spec.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
|
|
496
|
-
"",
|
|
497
|
-
].join("\n");
|
|
498
|
-
|
|
499
|
-
export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
500
|
-
[
|
|
501
|
-
`# Workflow Plan: ${plan.workflow}`,
|
|
502
|
-
"",
|
|
503
|
-
`- Mode: ${plan.mode}`,
|
|
504
|
-
`- Requires approval: ${plan.requiresApproval}`,
|
|
505
|
-
"",
|
|
506
|
-
"## Inputs",
|
|
507
|
-
...Object.entries(plan.inputs).map(
|
|
508
|
-
([name, value]) => `- \`${name}\`: ${Array.isArray(value) ? value.join(", ") : value}`,
|
|
509
|
-
),
|
|
510
|
-
"",
|
|
511
|
-
"## Optional Surfaces",
|
|
512
|
-
...Object.entries(plan.optionalSurfaces).map(([name, mode]) => `- \`${name}\`: ${mode}`),
|
|
513
|
-
"",
|
|
514
|
-
"## Steps",
|
|
515
|
-
...plan.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
|
|
516
|
-
"",
|
|
517
|
-
"## Predicted Changes",
|
|
518
|
-
...plan.predictedChanges.map((change) => {
|
|
519
|
-
const scope = change.applyScope ? ` (${change.applyScope})` : "";
|
|
520
|
-
return `- \`${change.action}\`${scope} ${change.target}: ${change.reason}`;
|
|
521
|
-
}),
|
|
522
|
-
"",
|
|
523
|
-
"## Validation",
|
|
524
|
-
...plan.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
|
|
525
|
-
"",
|
|
526
|
-
"## Diagnostics",
|
|
527
|
-
...(plan.diagnostics.length
|
|
528
|
-
? plan.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
529
|
-
: ["- none"]),
|
|
530
|
-
"",
|
|
531
|
-
"## Recommendations",
|
|
532
|
-
...(plan.recommendations.length
|
|
533
|
-
? plan.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
|
|
534
|
-
: ["- none"]),
|
|
535
|
-
"",
|
|
536
|
-
].join("\n");
|
|
537
|
-
|
|
538
|
-
const workflowStatus = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
539
|
-
diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed";
|
|
540
|
-
|
|
541
|
-
const commandStatus = (commands: readonly WorkflowValidationCommandResult[]) =>
|
|
542
|
-
commands.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
543
|
-
|
|
544
|
-
const uniqueBy = <T>(values: readonly T[], key: (value: T) => string) => {
|
|
545
|
-
const seen = new Set<string>();
|
|
546
|
-
return values.filter((value) => {
|
|
547
|
-
const itemKey = key(value);
|
|
548
|
-
if (seen.has(itemKey)) return false;
|
|
549
|
-
seen.add(itemKey);
|
|
550
|
-
return true;
|
|
551
|
-
});
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
export const createWorkflowApplyReport = ({
|
|
555
|
-
workflow,
|
|
556
|
-
mode,
|
|
557
|
-
changedFiles = [],
|
|
558
|
-
generatedFiles = [],
|
|
559
|
-
appliedCommands = [],
|
|
560
|
-
recommendedValidationCommands,
|
|
561
|
-
commands = [],
|
|
562
|
-
diagnostics = [],
|
|
563
|
-
recommendations = [],
|
|
564
|
-
nextActions = [],
|
|
565
|
-
plan,
|
|
566
|
-
}: Omit<
|
|
567
|
-
WorkflowApplyReport,
|
|
568
|
-
| "schemaVersion"
|
|
569
|
-
| "runId"
|
|
570
|
-
| "applyReportPath"
|
|
571
|
-
| "validationTarget"
|
|
572
|
-
| "status"
|
|
573
|
-
| "appliedCommands"
|
|
574
|
-
| "recommendedValidationCommands"
|
|
575
|
-
| "commands"
|
|
576
|
-
| "recommendations"
|
|
577
|
-
> & {
|
|
578
|
-
appliedCommands?: WorkflowApplyCommand[];
|
|
579
|
-
recommendedValidationCommands?: WorkflowApplyCommand[];
|
|
580
|
-
commands?: WorkflowApplyCommand[];
|
|
581
|
-
recommendations?: WorkflowRecommendation[];
|
|
582
|
-
}): WorkflowApplyReport => {
|
|
583
|
-
const validationCommands = recommendedValidationCommands ?? commands;
|
|
584
|
-
return {
|
|
585
|
-
schemaVersion: 1,
|
|
586
|
-
workflow,
|
|
587
|
-
mode,
|
|
588
|
-
status: workflowStatus(diagnostics),
|
|
589
|
-
changedFiles: uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
|
|
590
|
-
generatedFiles: uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
|
|
591
|
-
appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
|
|
592
|
-
recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
|
|
593
|
-
commands: uniqueBy(validationCommands, (command) => command.command),
|
|
594
|
-
diagnostics,
|
|
595
|
-
recommendations: uniqueBy(recommendations, (recommendation) => `${recommendation.kind}:${recommendation.code}`),
|
|
596
|
-
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
597
|
-
plan,
|
|
598
|
-
};
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
|
|
602
|
-
const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
|
|
603
|
-
return command
|
|
604
|
-
.replaceAll("<app-or-lib-or-pkg>", target)
|
|
605
|
-
.replaceAll("<app-or-lib>", target)
|
|
606
|
-
.replaceAll("<app-name>", target);
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
|
|
610
|
-
plan.validation.map((validation) => ({
|
|
611
|
-
command: resolveWorkflowCommand(validation.command, plan),
|
|
612
|
-
reason: validation.reason,
|
|
613
|
-
kind: validation.kind,
|
|
614
|
-
})) satisfies WorkflowApplyCommand[];
|
|
615
|
-
|
|
616
|
-
export const workflowRunsDir = ".akan/workflows/runs";
|
|
617
|
-
|
|
618
|
-
export const createWorkflowRunId = (prefix = "run") =>
|
|
619
|
-
`${prefix}-${new Date()
|
|
620
|
-
.toISOString()
|
|
621
|
-
.replace(/[-:.TZ]/g, "")
|
|
622
|
-
.slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
623
|
-
|
|
624
|
-
const getRunId = (artifact: WorkflowRunArtifact) => {
|
|
625
|
-
if ("runId" in artifact && artifact.runId) return artifact.runId;
|
|
626
|
-
return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
|
|
630
|
-
|
|
631
|
-
const withWorkflowRunMetadata = (
|
|
632
|
-
artifact: WorkflowRunArtifact,
|
|
633
|
-
runId: string,
|
|
634
|
-
artifactPath: string,
|
|
635
|
-
): WorkflowRunArtifact => {
|
|
636
|
-
if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
|
|
637
|
-
return { ...artifact, runId, applyReportPath: artifactPath, validationTarget: artifactPath };
|
|
638
|
-
}
|
|
639
|
-
if ("kind" in artifact) return { ...artifact, runId, repairReportPath: artifactPath };
|
|
640
|
-
return artifact;
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
|
|
644
|
-
const runId = getRunId(artifact);
|
|
645
|
-
const artifactPath = workflowRunArtifactPath(runId);
|
|
646
|
-
const artifactWithMetadata = withWorkflowRunMetadata(artifact, runId, artifactPath);
|
|
647
|
-
await workspace.writeFile(artifactPath, jsonText(artifactWithMetadata), { silent: true });
|
|
648
|
-
return { runId, path: artifactPath, artifact: artifactWithMetadata };
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
export const workflowSyncDir = ".akan/workflows/sync";
|
|
652
|
-
|
|
653
|
-
const syncStateSlug = (target: string) =>
|
|
654
|
-
target
|
|
655
|
-
.trim()
|
|
656
|
-
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
657
|
-
.replace(/^-+|-+$/g, "")
|
|
658
|
-
.toLowerCase();
|
|
659
|
-
|
|
660
|
-
export const workflowSyncStatePath = (target: string) => `${workflowSyncDir}/${syncStateSlug(target) || "target"}.json`;
|
|
661
|
-
|
|
662
|
-
export const generatedFilePathsForTarget = (targetRoot: string, reason = "Generated files were refreshed by sync.") =>
|
|
663
|
-
[
|
|
664
|
-
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
665
|
-
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
666
|
-
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
667
|
-
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason },
|
|
668
|
-
] satisfies PrimitiveGeneratedFile[];
|
|
669
|
-
|
|
670
|
-
export const writeGeneratedSyncState = async (workspace: Workspace, state: GeneratedSyncState) => {
|
|
671
|
-
const statePath = workflowSyncStatePath(state.target);
|
|
672
|
-
await workspace.writeFile(statePath, jsonText(state), { silent: true });
|
|
673
|
-
return statePath;
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
|
|
677
|
-
const artifactPath = workflowRunArtifactPath(runId);
|
|
678
|
-
if (!(await workspace.exists(artifactPath))) throw new Error(`Workflow run artifact does not exist: ${artifactPath}`);
|
|
679
|
-
return (await workspace.readJson(artifactPath)) as WorkflowRunArtifact;
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
export type WorkflowValidationCommandExecutor = (
|
|
683
|
-
command: WorkflowApplyCommand,
|
|
684
|
-
) => Promise<WorkflowValidationCommandResult>;
|
|
685
|
-
|
|
686
|
-
export const createWorkflowValidationRunReport = async ({
|
|
687
|
-
runId = createWorkflowRunId("validation"),
|
|
688
|
-
workflow,
|
|
689
|
-
source,
|
|
690
|
-
plan,
|
|
691
|
-
commands,
|
|
692
|
-
execute,
|
|
693
|
-
diagnostics = [],
|
|
694
|
-
baselineDiagnostics = [],
|
|
695
|
-
workflowDiagnostics = [],
|
|
696
|
-
repairActions = [],
|
|
697
|
-
}: {
|
|
698
|
-
runId?: string;
|
|
699
|
-
workflow: string;
|
|
700
|
-
source: WorkflowRunSource;
|
|
701
|
-
plan?: WorkflowPlan;
|
|
702
|
-
commands: WorkflowApplyCommand[];
|
|
703
|
-
execute: WorkflowValidationCommandExecutor;
|
|
704
|
-
diagnostics?: WorkflowDiagnostic[];
|
|
705
|
-
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
706
|
-
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
707
|
-
repairActions?: RepairAction[];
|
|
708
|
-
}): Promise<WorkflowValidationRunReport> => {
|
|
709
|
-
const results: WorkflowValidationCommandResult[] = [];
|
|
710
|
-
for (const command of commands) {
|
|
711
|
-
results.push(await execute(command));
|
|
712
|
-
}
|
|
713
|
-
const commandDiagnostics = results.flatMap((result) =>
|
|
714
|
-
result.status === "failed"
|
|
715
|
-
? [
|
|
716
|
-
{
|
|
717
|
-
severity: "error" as const,
|
|
718
|
-
code: "workflow-validation-command-failed",
|
|
719
|
-
message: `Validation command failed: ${result.command}`,
|
|
720
|
-
command: result.command,
|
|
721
|
-
kind: result.kind,
|
|
722
|
-
failureScope: result.failureScope,
|
|
723
|
-
},
|
|
724
|
-
]
|
|
725
|
-
: [],
|
|
726
|
-
);
|
|
727
|
-
return {
|
|
728
|
-
schemaVersion: 1,
|
|
729
|
-
runId,
|
|
730
|
-
workflow,
|
|
731
|
-
mode: "validate",
|
|
732
|
-
source,
|
|
733
|
-
status: workflowStatus([...diagnostics, ...commandDiagnostics]) === "failed" ? "failed" : commandStatus(results),
|
|
734
|
-
commands: results,
|
|
735
|
-
diagnostics: [...diagnostics, ...commandDiagnostics],
|
|
736
|
-
baselineDiagnostics,
|
|
737
|
-
workflowDiagnostics,
|
|
738
|
-
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
739
|
-
nextActions: results
|
|
740
|
-
.filter((result) => result.status === "failed")
|
|
741
|
-
.map((result) => ({ command: result.command, reason: "Re-run this validation command after repair." })),
|
|
742
|
-
plan,
|
|
743
|
-
};
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
747
|
-
const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
748
|
-
if (change.action !== "create" && change.action !== "modify") return [];
|
|
749
|
-
return [
|
|
750
|
-
{
|
|
751
|
-
path: change.target,
|
|
752
|
-
action: change.action,
|
|
753
|
-
reason: change.reason,
|
|
754
|
-
},
|
|
755
|
-
];
|
|
756
|
-
});
|
|
757
|
-
const generatedFiles: PrimitiveGeneratedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
758
|
-
if (change.action !== "sync") return [];
|
|
759
|
-
return [
|
|
760
|
-
{
|
|
761
|
-
path: change.target,
|
|
762
|
-
action: "sync",
|
|
763
|
-
reason: change.reason,
|
|
764
|
-
},
|
|
765
|
-
];
|
|
766
|
-
});
|
|
767
|
-
const diagnostics = [...plan.diagnostics];
|
|
768
|
-
return createWorkflowApplyReport({
|
|
769
|
-
workflow: plan.workflow,
|
|
770
|
-
mode: "dry-run",
|
|
771
|
-
changedFiles,
|
|
772
|
-
generatedFiles,
|
|
773
|
-
commands: workflowCommandsForPlan(plan),
|
|
774
|
-
diagnostics,
|
|
775
|
-
recommendations: plan.recommendations,
|
|
776
|
-
nextActions: workflowCommandsForPlan(plan),
|
|
777
|
-
plan,
|
|
778
|
-
});
|
|
779
|
-
};
|
|
780
|
-
|
|
781
|
-
export const workflowStepKey = (workflow: string, stepId: string) => `${workflow}:${stepId}`;
|
|
782
|
-
|
|
783
|
-
export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport): WorkflowStepResult => ({
|
|
784
|
-
changedFiles: report.changedFiles,
|
|
785
|
-
generatedFiles: report.generatedFiles,
|
|
786
|
-
commands: report.validationCommands,
|
|
787
|
-
diagnostics: report.diagnostics,
|
|
788
|
-
recommendations: [],
|
|
789
|
-
nextActions: report.nextActions,
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
const workflowStringInput = (value: WorkflowInputValue | undefined) => (typeof value === "string" ? value : null);
|
|
793
|
-
const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
|
|
794
|
-
Array.isArray(value) ? value.join(",") : null;
|
|
795
|
-
|
|
796
|
-
const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
797
|
-
if (!target) return null;
|
|
798
|
-
const [apps, libs] = await workspace.getSyss();
|
|
799
|
-
if (apps.includes(target)) return AppExecutor.from(workspace, target);
|
|
800
|
-
if (libs.includes(target)) return LibExecutor.from(workspace, target);
|
|
801
|
-
return null;
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
const targetMissing = (input = "app"): WorkflowDiagnostic => ({
|
|
805
|
-
severity: "error",
|
|
806
|
-
code: "workflow-target-missing",
|
|
807
|
-
input,
|
|
808
|
-
message: "Workflow target app or library was not found.",
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
const inputMissing = (input: string): WorkflowDiagnostic => ({
|
|
812
|
-
severity: "error",
|
|
813
|
-
code: "workflow-input-missing",
|
|
814
|
-
input,
|
|
815
|
-
message: `Workflow input "${input}" is required for apply.`,
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
const unsupportedInput = (input: string, message: string): WorkflowDiagnostic => ({
|
|
819
|
-
severity: "error",
|
|
820
|
-
code: "workflow-input-unsupported",
|
|
821
|
-
input,
|
|
822
|
-
message,
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
|
|
826
|
-
const module = workflowStringInput(plan.inputs.module) ?? "<module>";
|
|
827
|
-
const field = workflowStringInput(plan.inputs.field) ?? "<field>";
|
|
828
|
-
const moduleClassName = capitalize(module);
|
|
829
|
-
const target = `*/lib/${module}/${moduleClassName}.Template.tsx`;
|
|
830
|
-
return {
|
|
831
|
-
recommendations: [
|
|
832
|
-
{
|
|
833
|
-
code: "add-field-ui-surface-review",
|
|
834
|
-
kind: "manual-action",
|
|
835
|
-
target,
|
|
836
|
-
action: `Add ${field} to ${moduleClassName} UI surfaces only when the local Field.* pattern is clear.`,
|
|
837
|
-
confidence: "medium",
|
|
838
|
-
message: `Review Template/Unit/View/Store surfaces for ${module}.${field}; no UI file was modified automatically.`,
|
|
839
|
-
},
|
|
840
|
-
],
|
|
841
|
-
nextActions: [
|
|
842
|
-
{
|
|
843
|
-
command: `akan workflow explain ${plan.workflow}`,
|
|
844
|
-
reason: "Review UI surface guidance before manually editing ambiguous UI files.",
|
|
845
|
-
},
|
|
846
|
-
],
|
|
847
|
-
};
|
|
848
|
-
};
|
|
849
|
-
|
|
850
|
-
export const createWorkflowStepRegistry = ({
|
|
851
|
-
workspace,
|
|
852
|
-
createModule,
|
|
853
|
-
createScalar,
|
|
854
|
-
createUi,
|
|
855
|
-
addField,
|
|
856
|
-
addEnumField,
|
|
857
|
-
}: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
|
|
858
|
-
const inspect = async () => undefined;
|
|
859
|
-
const commandOnly = async () => undefined;
|
|
860
|
-
|
|
861
|
-
return {
|
|
862
|
-
inspectSystem: inspect,
|
|
863
|
-
inspectModule: inspect,
|
|
864
|
-
syncTarget: commandOnly,
|
|
865
|
-
lintTarget: commandOnly,
|
|
866
|
-
[workflowStepKey("create-module", "create-module")]: async (_step, plan) => {
|
|
867
|
-
const app = workflowStringInput(plan.inputs.app);
|
|
868
|
-
const module = workflowStringInput(plan.inputs.module);
|
|
869
|
-
const sys = await resolveWorkflowSys(workspace, app);
|
|
870
|
-
if (!sys || !module) return { diagnostics: [!sys ? targetMissing() : inputMissing("module")] };
|
|
871
|
-
return primitiveReportToWorkflowStepResult(await createModule(sys, module));
|
|
872
|
-
},
|
|
873
|
-
[workflowStepKey("create-scalar", "create-scalar")]: async (_step, plan) => {
|
|
874
|
-
const app = workflowStringInput(plan.inputs.app);
|
|
875
|
-
const scalar = workflowStringInput(plan.inputs.scalar);
|
|
876
|
-
const sys = await resolveWorkflowSys(workspace, app);
|
|
877
|
-
if (!sys || !scalar) return { diagnostics: [!sys ? targetMissing() : inputMissing("scalar")] };
|
|
878
|
-
return primitiveReportToWorkflowStepResult(await createScalar(sys, scalar));
|
|
879
|
-
},
|
|
880
|
-
[workflowStepKey("create-ui", "create-ui")]: async (_step, plan) => {
|
|
881
|
-
const surface = workflowStringInput(plan.inputs.surface);
|
|
882
|
-
if (surface !== "view" && surface !== "unit" && surface !== "template") {
|
|
883
|
-
return {
|
|
884
|
-
diagnostics: [
|
|
885
|
-
unsupportedInput("surface", "Workflow apply currently supports create-ui surfaces: view, unit, template."),
|
|
886
|
-
],
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
return primitiveReportToWorkflowStepResult(
|
|
890
|
-
await createUi({
|
|
891
|
-
app: workflowStringInput(plan.inputs.app),
|
|
892
|
-
module: workflowStringInput(plan.inputs.module),
|
|
893
|
-
surface,
|
|
894
|
-
}),
|
|
895
|
-
);
|
|
896
|
-
},
|
|
897
|
-
[workflowStepKey("add-field", "update-constant")]: async (_step, plan) => {
|
|
898
|
-
if (workflowStringInput(plan.inputs.type)?.toLowerCase() === "enum") {
|
|
899
|
-
return primitiveReportToWorkflowStepResult(
|
|
900
|
-
await addEnumField({
|
|
901
|
-
app: workflowStringInput(plan.inputs.app),
|
|
902
|
-
module: workflowStringInput(plan.inputs.module),
|
|
903
|
-
field: workflowStringInput(plan.inputs.field),
|
|
904
|
-
values: workflowStringListInput(plan.inputs.values),
|
|
905
|
-
defaultValue: workflowStringInput(plan.inputs.default),
|
|
906
|
-
}),
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
return primitiveReportToWorkflowStepResult(
|
|
910
|
-
await addField({
|
|
911
|
-
app: workflowStringInput(plan.inputs.app),
|
|
912
|
-
module: workflowStringInput(plan.inputs.module),
|
|
913
|
-
field: workflowStringInput(plan.inputs.field),
|
|
914
|
-
type: workflowStringInput(plan.inputs.type),
|
|
915
|
-
defaultValue: workflowStringInput(plan.inputs.default),
|
|
916
|
-
}),
|
|
917
|
-
);
|
|
918
|
-
},
|
|
919
|
-
[workflowStepKey("add-field", "update-dictionary")]: inspect,
|
|
920
|
-
[workflowStepKey("add-field", "update-ui-surfaces")]: async (_step, plan) => addFieldUiSurfaceInspection(plan),
|
|
921
|
-
[workflowStepKey("add-enum-field", "update-constant")]: async (_step, plan) =>
|
|
922
|
-
primitiveReportToWorkflowStepResult(
|
|
923
|
-
await addEnumField({
|
|
924
|
-
app: workflowStringInput(plan.inputs.app),
|
|
925
|
-
module: workflowStringInput(plan.inputs.module),
|
|
926
|
-
field: workflowStringInput(plan.inputs.field),
|
|
927
|
-
values: workflowStringListInput(plan.inputs.values),
|
|
928
|
-
defaultValue: workflowStringInput(plan.inputs.default),
|
|
929
|
-
}),
|
|
930
|
-
),
|
|
931
|
-
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
932
|
-
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
933
|
-
};
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
export const createWorkflowStepCommandResult = (
|
|
937
|
-
step: WorkflowStep,
|
|
938
|
-
command: string,
|
|
939
|
-
reason: string,
|
|
940
|
-
): WorkflowStepResult => ({
|
|
941
|
-
commands: [{ command, reason, stepId: step.id }],
|
|
942
|
-
nextActions: [{ command, reason }],
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
|
|
946
|
-
[
|
|
947
|
-
`# Workflow Apply: ${report.workflow}`,
|
|
948
|
-
"",
|
|
949
|
-
`- Mode: ${report.mode}`,
|
|
950
|
-
`- Status: ${report.status}`,
|
|
951
|
-
"",
|
|
952
|
-
"## Changed Files",
|
|
953
|
-
...(report.changedFiles.length
|
|
954
|
-
? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
955
|
-
: ["- none"]),
|
|
956
|
-
"",
|
|
957
|
-
"## Generated Files",
|
|
958
|
-
...(report.generatedFiles.length
|
|
959
|
-
? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
960
|
-
: ["- none"]),
|
|
961
|
-
"",
|
|
962
|
-
"## Applied Commands",
|
|
963
|
-
...(report.appliedCommands.length
|
|
964
|
-
? report.appliedCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
|
|
965
|
-
: ["- none"]),
|
|
966
|
-
"",
|
|
967
|
-
"## Recommended Validation Commands",
|
|
968
|
-
...(report.recommendedValidationCommands.length
|
|
969
|
-
? report.recommendedValidationCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
|
|
970
|
-
: ["- none"]),
|
|
971
|
-
"",
|
|
972
|
-
"## Diagnostics",
|
|
973
|
-
...(report.diagnostics.length
|
|
974
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
975
|
-
: ["- none"]),
|
|
976
|
-
"",
|
|
977
|
-
"## Recommendations",
|
|
978
|
-
...(report.recommendations.length
|
|
979
|
-
? report.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
|
|
980
|
-
: ["- none"]),
|
|
981
|
-
"",
|
|
982
|
-
"## Next Actions",
|
|
983
|
-
...(report.nextActions.length
|
|
984
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
985
|
-
: ["- none"]),
|
|
986
|
-
"",
|
|
987
|
-
].join("\n");
|
|
988
|
-
|
|
989
|
-
export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
|
|
990
|
-
format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
|
|
991
|
-
|
|
992
|
-
export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
|
|
993
|
-
[
|
|
994
|
-
`# Workflow Validation: ${report.workflow}`,
|
|
995
|
-
"",
|
|
996
|
-
`- Run: ${report.runId}`,
|
|
997
|
-
`- Status: ${report.status}`,
|
|
998
|
-
"",
|
|
999
|
-
"## Commands",
|
|
1000
|
-
...(report.commands.length
|
|
1001
|
-
? report.commands.map((command) => {
|
|
1002
|
-
const scope = command.failureScope ? ` (${command.failureScope})` : "";
|
|
1003
|
-
return `- [${command.status}] \`${command.command}\`${scope}: ${command.reason}`;
|
|
1004
|
-
})
|
|
1005
|
-
: ["- none"]),
|
|
1006
|
-
"",
|
|
1007
|
-
"## Diagnostics",
|
|
1008
|
-
...(report.diagnostics.length
|
|
1009
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
1010
|
-
: ["- none"]),
|
|
1011
|
-
"",
|
|
1012
|
-
"## Repair Actions",
|
|
1013
|
-
...(report.repairActions.length
|
|
1014
|
-
? report.repairActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
1015
|
-
: ["- none"]),
|
|
1016
|
-
"",
|
|
1017
|
-
"## Next Actions",
|
|
1018
|
-
...(report.nextActions.length
|
|
1019
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
1020
|
-
: ["- none"]),
|
|
1021
|
-
"",
|
|
1022
|
-
].join("\n");
|
|
1023
|
-
|
|
1024
|
-
export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
|
|
1025
|
-
format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
|
|
1026
|
-
|
|
1027
|
-
export const renderWorkflowRunArtifact = (artifact: WorkflowRunArtifact, format: WorkflowFormat = "markdown") => {
|
|
1028
|
-
if ("kind" in artifact) return renderRepairReport(artifact, format);
|
|
1029
|
-
if ("mode" in artifact && artifact.mode === "validate") return renderWorkflowValidation(artifact, format);
|
|
1030
|
-
if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
|
|
1031
|
-
return renderWorkflowApply(artifact, format);
|
|
1032
|
-
}
|
|
1033
|
-
return jsonText(artifact);
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
export const createRepairReport = ({
|
|
1037
|
-
command,
|
|
1038
|
-
kind,
|
|
1039
|
-
target = null,
|
|
1040
|
-
diagnostics = [],
|
|
1041
|
-
repairActions = [],
|
|
1042
|
-
nextActions = [],
|
|
1043
|
-
commands = [],
|
|
1044
|
-
generatedFiles = [],
|
|
1045
|
-
syncedAt,
|
|
1046
|
-
}: Omit<
|
|
1047
|
-
RepairReport,
|
|
1048
|
-
| "schemaVersion"
|
|
1049
|
-
| "runId"
|
|
1050
|
-
| "repairReportPath"
|
|
1051
|
-
| "status"
|
|
1052
|
-
| "target"
|
|
1053
|
-
| "diagnostics"
|
|
1054
|
-
| "repairActions"
|
|
1055
|
-
| "nextActions"
|
|
1056
|
-
| "commands"
|
|
1057
|
-
> &
|
|
1058
|
-
Partial<
|
|
1059
|
-
Pick<
|
|
1060
|
-
RepairReport,
|
|
1061
|
-
"target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
|
|
1062
|
-
>
|
|
1063
|
-
>): RepairReport => ({
|
|
1064
|
-
schemaVersion: 1,
|
|
1065
|
-
command,
|
|
1066
|
-
kind,
|
|
1067
|
-
target,
|
|
1068
|
-
status: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed" ? "failed" : "passed",
|
|
1069
|
-
diagnostics,
|
|
1070
|
-
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
1071
|
-
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
1072
|
-
commands,
|
|
1073
|
-
generatedFiles,
|
|
1074
|
-
...(syncedAt ? { syncedAt } : {}),
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
export const renderRepairReportMarkdown = (report: RepairReport) =>
|
|
1078
|
-
[
|
|
1079
|
-
`# Akan Repair: ${report.kind}`,
|
|
1080
|
-
"",
|
|
1081
|
-
`- Status: ${report.status}`,
|
|
1082
|
-
`- Target: ${report.target ?? "none"}`,
|
|
1083
|
-
"",
|
|
1084
|
-
"## Commands",
|
|
1085
|
-
...(report.commands.length
|
|
1086
|
-
? report.commands.map((command) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
|
|
1087
|
-
: ["- none"]),
|
|
1088
|
-
"",
|
|
1089
|
-
"## Diagnostics",
|
|
1090
|
-
...(report.diagnostics.length
|
|
1091
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
1092
|
-
: ["- none"]),
|
|
1093
|
-
"",
|
|
1094
|
-
"## Next Actions",
|
|
1095
|
-
...(report.nextActions.length
|
|
1096
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
1097
|
-
: ["- none"]),
|
|
1098
|
-
"",
|
|
1099
|
-
].join("\n");
|
|
1100
|
-
|
|
1101
|
-
export const renderRepairReport = (report: RepairReport, format: WorkflowFormat = "markdown") =>
|
|
1102
|
-
format === "json" ? jsonText(report) : renderRepairReportMarkdown(report);
|
|
1103
|
-
|
|
1104
|
-
export class WorkflowExecutor {
|
|
1105
|
-
constructor(private readonly registry: WorkflowStepRegistry) {}
|
|
1106
|
-
|
|
1107
|
-
async apply(plan: WorkflowPlan): Promise<WorkflowApplyReport> {
|
|
1108
|
-
const changedFiles: PrimitiveChangedFile[] = [];
|
|
1109
|
-
const generatedFiles: PrimitiveGeneratedFile[] = [];
|
|
1110
|
-
const recommendedValidationCommands: WorkflowApplyCommand[] = [];
|
|
1111
|
-
const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
|
|
1112
|
-
const recommendations: WorkflowRecommendation[] = [...plan.recommendations];
|
|
1113
|
-
const nextActions: PrimitiveNextAction[] = [];
|
|
1114
|
-
|
|
1115
|
-
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
1116
|
-
return createWorkflowApplyReport({
|
|
1117
|
-
workflow: plan.workflow,
|
|
1118
|
-
mode: "apply",
|
|
1119
|
-
changedFiles,
|
|
1120
|
-
generatedFiles,
|
|
1121
|
-
recommendedValidationCommands,
|
|
1122
|
-
diagnostics,
|
|
1123
|
-
recommendations,
|
|
1124
|
-
nextActions,
|
|
1125
|
-
plan,
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
recommendedValidationCommands.push(...workflowCommandsForPlan(plan));
|
|
1130
|
-
nextActions.push(...workflowCommandsForPlan(plan));
|
|
1131
|
-
|
|
1132
|
-
for (const step of plan.steps) {
|
|
1133
|
-
const runner =
|
|
1134
|
-
this.registry[workflowStepKey(plan.workflow, step.id)] ?? this.registry[step.tool] ?? this.registry[step.id];
|
|
1135
|
-
if (!runner) {
|
|
1136
|
-
diagnostics.push({
|
|
1137
|
-
severity: "error",
|
|
1138
|
-
code: "workflow-step-unsupported",
|
|
1139
|
-
message: `Workflow ${plan.workflow} step "${step.id}" is not supported by workflow apply yet.`,
|
|
1140
|
-
});
|
|
1141
|
-
nextActions.push({
|
|
1142
|
-
command: `akan workflow explain ${plan.workflow}`,
|
|
1143
|
-
reason: "Review the unsupported workflow step before retrying apply.",
|
|
1144
|
-
});
|
|
1145
|
-
break;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
const result = await runner(step, plan);
|
|
1149
|
-
if (!result) continue;
|
|
1150
|
-
changedFiles.push(...(result.changedFiles ?? []));
|
|
1151
|
-
generatedFiles.push(...(result.generatedFiles ?? []));
|
|
1152
|
-
recommendedValidationCommands.push(...(result.commands ?? []));
|
|
1153
|
-
diagnostics.push(...(result.diagnostics ?? []));
|
|
1154
|
-
recommendations.push(...(result.recommendations ?? []));
|
|
1155
|
-
nextActions.push(...(result.nextActions ?? []));
|
|
1156
|
-
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return createWorkflowApplyReport({
|
|
1160
|
-
workflow: plan.workflow,
|
|
1161
|
-
mode: "apply",
|
|
1162
|
-
changedFiles,
|
|
1163
|
-
generatedFiles,
|
|
1164
|
-
recommendedValidationCommands,
|
|
1165
|
-
diagnostics,
|
|
1166
|
-
recommendations,
|
|
1167
|
-
nextActions,
|
|
1168
|
-
plan,
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
export const createPrimitiveWriteReport = ({
|
|
1174
|
-
command,
|
|
1175
|
-
status,
|
|
1176
|
-
changedFiles = [],
|
|
1177
|
-
generatedFiles = [],
|
|
1178
|
-
validationCommands = [],
|
|
1179
|
-
diagnostics = [],
|
|
1180
|
-
nextActions = [],
|
|
1181
|
-
}: Omit<PrimitiveWriteReport, "schemaVersion" | "status"> & {
|
|
1182
|
-
status?: PrimitiveWriteReport["status"];
|
|
1183
|
-
}): PrimitiveWriteReport => ({
|
|
1184
|
-
schemaVersion: 1,
|
|
1185
|
-
command,
|
|
1186
|
-
status: status ?? (diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed"),
|
|
1187
|
-
changedFiles,
|
|
1188
|
-
generatedFiles,
|
|
1189
|
-
validationCommands,
|
|
1190
|
-
diagnostics,
|
|
1191
|
-
nextActions,
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
export const renderPrimitiveWriteReport = (report: PrimitiveWriteReport) =>
|
|
1195
|
-
[
|
|
1196
|
-
`# Primitive Write: ${report.command}`,
|
|
1197
|
-
"",
|
|
1198
|
-
`- Status: ${report.status}`,
|
|
1199
|
-
"",
|
|
1200
|
-
"## Changed Files",
|
|
1201
|
-
...(report.changedFiles.length
|
|
1202
|
-
? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
1203
|
-
: ["- none"]),
|
|
1204
|
-
"",
|
|
1205
|
-
"## Generated Files",
|
|
1206
|
-
...(report.generatedFiles.length
|
|
1207
|
-
? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
1208
|
-
: ["- none"]),
|
|
1209
|
-
"",
|
|
1210
|
-
"## Validation Commands",
|
|
1211
|
-
...(report.validationCommands.length
|
|
1212
|
-
? report.validationCommands.map((validation) => `- \`${validation.command}\`: ${validation.reason}`)
|
|
1213
|
-
: ["- none"]),
|
|
1214
|
-
"",
|
|
1215
|
-
"## Diagnostics",
|
|
1216
|
-
...(report.diagnostics.length
|
|
1217
|
-
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
1218
|
-
: ["- none"]),
|
|
1219
|
-
"",
|
|
1220
|
-
"## Next Actions",
|
|
1221
|
-
...(report.nextActions.length
|
|
1222
|
-
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
1223
|
-
: ["- none"]),
|
|
1224
|
-
"",
|
|
1225
|
-
].join("\n");
|
|
1226
|
-
|
|
1227
|
-
export const renderPrimitiveReport = (report: PrimitiveWriteReport, format: PrimitiveFormat = "markdown") =>
|
|
1228
|
-
format === "json" ? jsonText(report) : renderPrimitiveWriteReport(report);
|
|
1229
|
-
|
|
1230
|
-
export const getSysRoot = (sys: Sys) => `${sys.type}s/${sys.name}`;
|
|
1231
|
-
|
|
1232
|
-
export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile["action"], reason: string) => ({
|
|
1233
|
-
path: `${getSysRoot(sys)}/${path}`,
|
|
1234
|
-
action,
|
|
1235
|
-
reason,
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
|
|
1239
|
-
generatedFilePathsForTarget(getSysRoot(sys), reason);
|
|
1240
|
-
|
|
1241
|
-
export const validationCommandsForTarget = (target: string) =>
|
|
1242
|
-
[
|
|
1243
|
-
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files from source conventions." },
|
|
1244
|
-
{ command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
|
|
1245
|
-
] satisfies PrimitiveValidationCommand[];
|
|
1246
|
-
|
|
1247
|
-
export const nextActionsForTarget = (target: string) =>
|
|
1248
|
-
[
|
|
1249
|
-
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files after source changes." },
|
|
1250
|
-
{ command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
|
|
1251
|
-
] satisfies PrimitiveNextAction[];
|
|
1252
|
-
|
|
1253
|
-
export const createPassedPrimitiveReport = ({
|
|
1254
|
-
command,
|
|
1255
|
-
changedFiles,
|
|
1256
|
-
generatedFiles,
|
|
1257
|
-
target,
|
|
1258
|
-
nextActions,
|
|
1259
|
-
}: {
|
|
1260
|
-
command: string;
|
|
1261
|
-
changedFiles: PrimitiveChangedFile[];
|
|
1262
|
-
generatedFiles?: PrimitiveGeneratedFile[];
|
|
1263
|
-
target: string;
|
|
1264
|
-
nextActions?: PrimitiveNextAction[];
|
|
1265
|
-
}) =>
|
|
1266
|
-
createPrimitiveWriteReport({
|
|
1267
|
-
command,
|
|
1268
|
-
changedFiles,
|
|
1269
|
-
generatedFiles: generatedFiles ?? [],
|
|
1270
|
-
validationCommands: validationCommandsForTarget(target),
|
|
1271
|
-
diagnostics: [],
|
|
1272
|
-
nextActions: nextActions ?? nextActionsForTarget(target),
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
export const scalarChangedFiles = (sys: Sys, scalarName: string, files: PrimitiveFileMap) =>
|
|
1276
|
-
Object.values(files).map((file) =>
|
|
1277
|
-
sourceFile(sys, `lib/__scalar/${scalarName}/${file.filename}`, "create", "Scalar source file was created."),
|
|
1278
|
-
);
|
|
1279
|
-
|
|
1280
|
-
export const titleize = (value: string) =>
|
|
1281
|
-
value
|
|
1282
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
1283
|
-
.replace(/[-_]+/g, " ")
|
|
1284
|
-
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1285
|
-
|
|
1286
|
-
export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
|
|
1287
|
-
|
|
1288
|
-
export const compactDiagnostics = (diagnostics: Array<WorkflowDiagnostic | false | null | undefined>) =>
|
|
1289
|
-
diagnostics.filter((diagnostic): diagnostic is WorkflowDiagnostic => !!diagnostic);
|
|
1290
|
-
|
|
1291
|
-
export const normalizeFieldType = (typeName: string) => {
|
|
1292
|
-
const normalizedTypes: Record<string, string> = {
|
|
1293
|
-
string: "String",
|
|
1294
|
-
boolean: "Boolean",
|
|
1295
|
-
date: "Date",
|
|
1296
|
-
int: "Int",
|
|
1297
|
-
integer: "Int",
|
|
1298
|
-
float: "Float",
|
|
1299
|
-
double: "Float",
|
|
1300
|
-
decimal: "Float",
|
|
1301
|
-
};
|
|
1302
|
-
return normalizedTypes[typeName.toLowerCase()] ?? typeName;
|
|
1303
|
-
};
|
|
1304
|
-
|
|
1305
|
-
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
1306
|
-
if (typeName !== "Int" && typeName !== "Float") return content;
|
|
1307
|
-
if (new RegExp(`import \\{[^}]*\\b${typeName}\\b[^}]*\\} from "akanjs/base";`).test(content)) return content;
|
|
1308
|
-
const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
|
|
1309
|
-
if (baseImport) {
|
|
1310
|
-
const names = baseImport[1]
|
|
1311
|
-
.split(",")
|
|
1312
|
-
.map((name) => name.trim())
|
|
1313
|
-
.filter(Boolean);
|
|
1314
|
-
return content.replace(baseImport[0], `import { ${[...names, typeName].sort().join(", ")} } from "akanjs/base";`);
|
|
1315
|
-
}
|
|
1316
|
-
return `import { ${typeName} } from "akanjs/base";\n${content}`;
|
|
1317
|
-
};
|
|
1318
|
-
|
|
1319
|
-
export const fieldExpression = (typeName: string, defaultValue?: string | null) => {
|
|
1320
|
-
const typeExpression = normalizeFieldType(typeName);
|
|
1321
|
-
const defaultOption = defaultValue ? `, { default: ${JSON.stringify(defaultValue)} }` : "";
|
|
1322
|
-
return `field(${typeExpression}${defaultOption})`;
|
|
1323
|
-
};
|
|
1324
|
-
|
|
1325
|
-
export const insertIntoObject = (content: string, className: string, line: string) => {
|
|
1326
|
-
const classIndex = content.indexOf(`export class ${className} extends via`);
|
|
1327
|
-
if (classIndex < 0) return null;
|
|
1328
|
-
const objectEndIndex = content.indexOf("}))", classIndex);
|
|
1329
|
-
if (objectEndIndex < 0) return null;
|
|
1330
|
-
const prefix = content.slice(0, objectEndIndex);
|
|
1331
|
-
const suffix = content.slice(objectEndIndex);
|
|
1332
|
-
const insertion = prefix.endsWith("\n") ? ` ${line}\n` : `\n ${line}\n`;
|
|
1333
|
-
return `${prefix}${insertion}${suffix}`;
|
|
1334
|
-
};
|
|
1335
|
-
|
|
1336
|
-
export const ensureEnumImport = (content: string) => {
|
|
1337
|
-
if (content.includes("enumOf")) return content;
|
|
1338
|
-
const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
|
|
1339
|
-
if (baseImport) {
|
|
1340
|
-
const names = baseImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
1341
|
-
return content.replace(baseImport[0], `import { ${[...names, "enumOf"].sort().join(", ")} } from "akanjs/base";`);
|
|
1342
|
-
}
|
|
1343
|
-
return `import { enumOf } from "akanjs/base";\n${content}`;
|
|
1344
|
-
};
|
|
1345
|
-
|
|
1346
|
-
export const insertEnumClass = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
1347
|
-
if (content.includes(`export class ${enumClassName} extends enumOf`)) return content;
|
|
1348
|
-
const enumClass = `export class ${enumClassName} extends enumOf("${enumName}", [\n${values
|
|
1349
|
-
.map((value) => ` ${JSON.stringify(value)},`)
|
|
1350
|
-
.join("\n")}\n] as const) {}\n\n`;
|
|
1351
|
-
const firstClassIndex = content.indexOf("export class ");
|
|
1352
|
-
if (firstClassIndex < 0) return `${content}\n${enumClass}`;
|
|
1353
|
-
return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
|
|
1354
|
-
};
|
|
1355
|
-
|
|
1356
|
-
export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
1357
|
-
if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
|
|
1358
|
-
const label = titleize(fieldName);
|
|
1359
|
-
const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
|
|
1360
|
-
if (modelIndex < 0) return null;
|
|
1361
|
-
const objectEndIndex = content.indexOf(" }))", modelIndex);
|
|
1362
|
-
if (objectEndIndex < 0) return null;
|
|
1363
|
-
return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label)}, ${JSON.stringify(
|
|
1364
|
-
label,
|
|
1365
|
-
)}]).desc([${JSON.stringify(label)}, ${JSON.stringify(label)}]),\n${content.slice(objectEndIndex)}`;
|
|
1366
|
-
};
|
|
1367
|
-
|
|
1368
|
-
export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
|
|
1369
|
-
if (new RegExp(`import type \\{[^}]*\\b${typeName}\\b[^}]*\\} from "${constantPath}";`).test(content)) return content;
|
|
1370
|
-
const importPattern = new RegExp(`import type \\{ ([^}]+) \\} from "${constantPath}";`);
|
|
1371
|
-
const existingImport = content.match(importPattern);
|
|
1372
|
-
if (existingImport !== null) {
|
|
1373
|
-
const names = existingImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
1374
|
-
return content.replace(
|
|
1375
|
-
existingImport[0],
|
|
1376
|
-
`import type { ${[...names, typeName].sort().join(", ")} } from "${constantPath}";`,
|
|
1377
|
-
);
|
|
1378
|
-
}
|
|
1379
|
-
return `import type { ${typeName} } from "${constantPath}";\n${content}`;
|
|
1380
|
-
};
|
|
1381
|
-
|
|
1382
|
-
export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
1383
|
-
if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
|
|
1384
|
-
const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
|
|
1385
|
-
.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
|
|
1386
|
-
.join("\n")}\n }))\n`;
|
|
1387
|
-
const chainEndIndex = content.lastIndexOf(";");
|
|
1388
|
-
const insertBeforeIndex = [content.indexOf(".error("), content.indexOf(".translate("), chainEndIndex]
|
|
1389
|
-
.filter((index) => index >= 0)
|
|
1390
|
-
.sort((a, b) => a - b)[0];
|
|
1391
|
-
if (insertBeforeIndex === undefined) return null;
|
|
1392
|
-
return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
|
|
1393
|
-
};
|
|
1394
|
-
|
|
1395
|
-
export const parseValues = (value: string | null) =>
|
|
1396
|
-
value
|
|
1397
|
-
?.split(",")
|
|
1398
|
-
.map((item) => item.trim())
|
|
1399
|
-
.filter(Boolean) ?? [];
|
|
1
|
+
export * from "./artifacts";
|
|
2
|
+
export * from "./executor";
|
|
3
|
+
export * from "./plan";
|
|
4
|
+
export * from "./primitive";
|
|
5
|
+
export * from "./render";
|
|
6
|
+
export * from "./source";
|
|
7
|
+
export * from "./types";
|
|
8
|
+
export * from "./uiPolicy";
|
|
9
|
+
export * from "./utils";
|