@akanjs/devkit 2.3.9-rc.2 → 2.3.9-rc.3
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 +187 -12
- package/package.json +2 -2
- package/workflow/index.ts +251 -14
package/akanContext.ts
CHANGED
|
@@ -4,7 +4,15 @@ import { capitalize } from "akanjs/common";
|
|
|
4
4
|
import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
|
|
5
5
|
import { FileSys } from "./fileSys";
|
|
6
6
|
import type { PackageJson } from "./types";
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
type GeneratedSyncState,
|
|
9
|
+
type RepairAction,
|
|
10
|
+
type WorkflowApplyReport,
|
|
11
|
+
type WorkflowPlan,
|
|
12
|
+
type WorkflowRunArtifact,
|
|
13
|
+
workflowRunArtifactPath,
|
|
14
|
+
workflowSyncDir,
|
|
15
|
+
} from "./workflow";
|
|
8
16
|
|
|
9
17
|
export type AkanContextFormat = "json" | "markdown";
|
|
10
18
|
export type AkanModuleKind = "domain" | "service" | "scalar";
|
|
@@ -61,6 +69,29 @@ export interface AkanDiagnostic {
|
|
|
61
69
|
message: string;
|
|
62
70
|
path?: string;
|
|
63
71
|
repairActions?: RepairAction[];
|
|
72
|
+
scope?: "baseline" | "workflow" | "unknown";
|
|
73
|
+
context?: {
|
|
74
|
+
workflow?: string;
|
|
75
|
+
planPath?: string;
|
|
76
|
+
runId?: string;
|
|
77
|
+
target?: string;
|
|
78
|
+
paths?: string[];
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GeneratedFilesFreshness {
|
|
83
|
+
status: "fresh" | "stale" | "missing" | "unknown";
|
|
84
|
+
message: string;
|
|
85
|
+
refreshCommand: string;
|
|
86
|
+
verifyingCommands: string[];
|
|
87
|
+
targets?: {
|
|
88
|
+
target: string;
|
|
89
|
+
status: "fresh" | "stale" | "missing" | "unknown";
|
|
90
|
+
lastSyncedAt?: string;
|
|
91
|
+
runId?: string;
|
|
92
|
+
generatedFiles: string[];
|
|
93
|
+
reason: string;
|
|
94
|
+
}[];
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
export interface AkanDoctorResult {
|
|
@@ -71,14 +102,11 @@ export interface AkanDoctorResult {
|
|
|
71
102
|
status: "passed" | "failed";
|
|
72
103
|
diagnostics: AkanDiagnostic[];
|
|
73
104
|
generatedFiles: string[];
|
|
74
|
-
generatedFilesFreshness:
|
|
75
|
-
status: "unknown";
|
|
76
|
-
message: string;
|
|
77
|
-
refreshCommand: string;
|
|
78
|
-
verifyingCommands: string[];
|
|
79
|
-
};
|
|
105
|
+
generatedFilesFreshness: GeneratedFilesFreshness;
|
|
80
106
|
validationCommands: string[];
|
|
81
107
|
repairActions: RepairAction[];
|
|
108
|
+
baselineDiagnostics?: AkanDiagnostic[];
|
|
109
|
+
workflowDiagnostics?: AkanDiagnostic[];
|
|
82
110
|
}
|
|
83
111
|
|
|
84
112
|
export type JsonRpcRequest = {
|
|
@@ -136,6 +164,7 @@ export const renderDoctorText = (result: AkanDoctorResult) => {
|
|
|
136
164
|
lines.push(
|
|
137
165
|
"",
|
|
138
166
|
"Generated file freshness:",
|
|
167
|
+
`Status: ${result.generatedFilesFreshness.status}`,
|
|
139
168
|
result.generatedFilesFreshness.message,
|
|
140
169
|
`Refresh: ${result.generatedFilesFreshness.refreshCommand}`,
|
|
141
170
|
"",
|
|
@@ -182,7 +211,7 @@ const validationCommands = [
|
|
|
182
211
|
"akan doctor --strict --format json",
|
|
183
212
|
];
|
|
184
213
|
|
|
185
|
-
const
|
|
214
|
+
const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
|
|
186
215
|
status: "unknown" as const,
|
|
187
216
|
message: "Run sync before validation so generated Akan files match the current source conventions.",
|
|
188
217
|
refreshCommand: "akan sync <app-or-lib>",
|
|
@@ -269,6 +298,135 @@ const safeReadJson = async <T>(filePath: string) => {
|
|
|
269
298
|
}
|
|
270
299
|
};
|
|
271
300
|
|
|
301
|
+
const isWorkflowPlan = (value: unknown): value is WorkflowPlan =>
|
|
302
|
+
typeof value === "object" &&
|
|
303
|
+
value !== null &&
|
|
304
|
+
"schemaVersion" in value &&
|
|
305
|
+
value.schemaVersion === 1 &&
|
|
306
|
+
"mode" in value &&
|
|
307
|
+
value.mode === "plan";
|
|
308
|
+
|
|
309
|
+
const isWorkflowApplyReport = (value: unknown): value is WorkflowApplyReport =>
|
|
310
|
+
typeof value === "object" &&
|
|
311
|
+
value !== null &&
|
|
312
|
+
"schemaVersion" in value &&
|
|
313
|
+
value.schemaVersion === 1 &&
|
|
314
|
+
"mode" in value &&
|
|
315
|
+
(value.mode === "apply" || value.mode === "dry-run");
|
|
316
|
+
|
|
317
|
+
const isWorkflowRunArtifact = (value: unknown): value is WorkflowRunArtifact =>
|
|
318
|
+
typeof value === "object" && value !== null && "schemaVersion" in value && value.schemaVersion === 1;
|
|
319
|
+
|
|
320
|
+
const planInputString = (plan: WorkflowPlan, key: string) => {
|
|
321
|
+
const value = plan.inputs[key];
|
|
322
|
+
return typeof value === "string" ? value : "";
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const expandWorkflowTarget = (target: string, plan: WorkflowPlan) => {
|
|
326
|
+
const app = planInputString(plan, "app");
|
|
327
|
+
const module = planInputString(plan, "module");
|
|
328
|
+
const moduleClass = module ? capitalize(module) : "<Module>";
|
|
329
|
+
return target
|
|
330
|
+
.replace(/^\*\//, app ? `apps/${app}/` : "")
|
|
331
|
+
.replaceAll("<module>", module || "<module>")
|
|
332
|
+
.replaceAll("<Module>", moduleClass);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const workflowPathsForPlan = (plan: WorkflowPlan) =>
|
|
336
|
+
plan.predictedChanges.map((change) => expandWorkflowTarget(change.target, plan));
|
|
337
|
+
|
|
338
|
+
const workflowPathsForArtifact = (artifact: WorkflowRunArtifact) => {
|
|
339
|
+
if (isWorkflowPlan(artifact)) return workflowPathsForPlan(artifact);
|
|
340
|
+
if (isWorkflowApplyReport(artifact)) {
|
|
341
|
+
return [
|
|
342
|
+
...artifact.changedFiles.map((file) => file.path),
|
|
343
|
+
...artifact.generatedFiles.map((file) => file.path),
|
|
344
|
+
...workflowPathsForPlan(artifact.plan),
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
if ("mode" in artifact && artifact.mode === "validate" && artifact.plan) return workflowPathsForPlan(artifact.plan);
|
|
348
|
+
return [];
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const loadWorkflowContextPaths = async (
|
|
352
|
+
workspace: WorkspaceExecutor,
|
|
353
|
+
runIdOrPlan: string | null,
|
|
354
|
+
changedFiles: string[],
|
|
355
|
+
) => {
|
|
356
|
+
const paths = [...changedFiles];
|
|
357
|
+
if (!runIdOrPlan) return paths;
|
|
358
|
+
const inputPath = path.isAbsolute(runIdOrPlan) ? runIdOrPlan : path.join(workspace.workspaceRoot, runIdOrPlan);
|
|
359
|
+
const artifact =
|
|
360
|
+
(await safeReadJson<WorkflowRunArtifact | WorkflowPlan>(inputPath)) ??
|
|
361
|
+
(await safeReadJson<WorkflowRunArtifact>(path.join(workspace.workspaceRoot, workflowRunArtifactPath(runIdOrPlan))));
|
|
362
|
+
if (artifact && isWorkflowRunArtifact(artifact)) paths.push(...workflowPathsForArtifact(artifact));
|
|
363
|
+
return [...new Set(paths.filter(Boolean))];
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const pathKey = (value: string) =>
|
|
367
|
+
value
|
|
368
|
+
.replaceAll("\\", "/")
|
|
369
|
+
.replaceAll("*", "")
|
|
370
|
+
.replace(/<[^>]+>/g, "")
|
|
371
|
+
.replace(/\/+/g, "/")
|
|
372
|
+
.replace(/^\/|\/$/g, "");
|
|
373
|
+
|
|
374
|
+
const isWorkflowRelatedDiagnostic = (diagnostic: AkanDiagnostic, workflowPaths: string[]) => {
|
|
375
|
+
if (!diagnostic.path) return false;
|
|
376
|
+
const diagnosticPath = pathKey(diagnostic.path);
|
|
377
|
+
return workflowPaths.some((workflowPath) => {
|
|
378
|
+
const candidate = pathKey(workflowPath);
|
|
379
|
+
if (!candidate) return false;
|
|
380
|
+
return (
|
|
381
|
+
diagnosticPath.startsWith(candidate) || candidate.startsWith(diagnosticPath) || diagnosticPath.includes(candidate)
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const readGeneratedSyncStates = async (workspace: WorkspaceExecutor) => {
|
|
387
|
+
const syncDir = path.join(workspace.workspaceRoot, workflowSyncDir);
|
|
388
|
+
const entries = await safeReadDir(syncDir);
|
|
389
|
+
const states = await Promise.all(
|
|
390
|
+
entries
|
|
391
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
392
|
+
.map((entry) => safeReadJson<GeneratedSyncState>(path.join(syncDir, entry.name))),
|
|
393
|
+
);
|
|
394
|
+
return states.filter(
|
|
395
|
+
(state): state is GeneratedSyncState =>
|
|
396
|
+
!!state && state.schemaVersion === 1 && typeof state.target === "string" && typeof state.syncedAt === "string",
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const generatedFreshnessFromStates = async (workspace: WorkspaceExecutor): Promise<GeneratedFilesFreshness> => {
|
|
401
|
+
const states = await readGeneratedSyncStates(workspace);
|
|
402
|
+
if (states.length === 0) return unknownGeneratedFilesFreshness;
|
|
403
|
+
const targets = states
|
|
404
|
+
.sort((a, b) => a.target.localeCompare(b.target))
|
|
405
|
+
.map((state) => ({
|
|
406
|
+
target: state.target,
|
|
407
|
+
status: state.status === "passed" ? ("fresh" as const) : ("stale" as const),
|
|
408
|
+
lastSyncedAt: state.syncedAt,
|
|
409
|
+
runId: state.runId,
|
|
410
|
+
generatedFiles: state.generatedFiles.map((file) => file.path),
|
|
411
|
+
reason:
|
|
412
|
+
state.status === "passed"
|
|
413
|
+
? `Generated files were refreshed by ${state.command}.`
|
|
414
|
+
: `Last generated repair command failed: ${state.command}.`,
|
|
415
|
+
}));
|
|
416
|
+
const lastFresh = targets
|
|
417
|
+
.filter((target) => target.status === "fresh" && target.lastSyncedAt)
|
|
418
|
+
.sort((a, b) => (b.lastSyncedAt ?? "").localeCompare(a.lastSyncedAt ?? ""))[0];
|
|
419
|
+
return {
|
|
420
|
+
status: targets.some((target) => target.status === "fresh") ? "fresh" : "stale",
|
|
421
|
+
message: lastFresh
|
|
422
|
+
? `Generated files were refreshed for ${lastFresh.target} at ${lastFresh.lastSyncedAt}.`
|
|
423
|
+
: "Generated sync state exists, but the last recorded repair did not pass.",
|
|
424
|
+
refreshCommand: "akan sync <app-or-lib>",
|
|
425
|
+
verifyingCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>"],
|
|
426
|
+
targets,
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
272
430
|
const parseAbstractSummary = (
|
|
273
431
|
relativePath: string,
|
|
274
432
|
content: string | null,
|
|
@@ -422,9 +580,14 @@ export class AkanContextAnalyzer {
|
|
|
422
580
|
|
|
423
581
|
static async doctor(
|
|
424
582
|
workspace: WorkspaceExecutor,
|
|
425
|
-
{
|
|
583
|
+
{
|
|
584
|
+
strict = false,
|
|
585
|
+
runIdOrPlan = null,
|
|
586
|
+
changedFiles = [],
|
|
587
|
+
}: { strict?: boolean; runIdOrPlan?: string | null; changedFiles?: string[] } = {},
|
|
426
588
|
): Promise<AkanDoctorResult> {
|
|
427
589
|
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
590
|
+
const workflowPaths = await loadWorkflowContextPaths(workspace, runIdOrPlan, changedFiles);
|
|
428
591
|
const diagnostics: AkanDiagnostic[] = [];
|
|
429
592
|
const repairActions: RepairAction[] = [
|
|
430
593
|
repairAction("generated", "akan repair generated --app <app-or-lib>", "Refresh generated Akan files.", true),
|
|
@@ -524,17 +687,29 @@ export class AkanContextAnalyzer {
|
|
|
524
687
|
}
|
|
525
688
|
}
|
|
526
689
|
|
|
690
|
+
const scopedDiagnostics = diagnostics.map((diagnostic) => ({
|
|
691
|
+
...diagnostic,
|
|
692
|
+
scope: workflowPaths.length
|
|
693
|
+
? isWorkflowRelatedDiagnostic(diagnostic, workflowPaths)
|
|
694
|
+
? ("workflow" as const)
|
|
695
|
+
: ("baseline" as const)
|
|
696
|
+
: diagnostic.scope,
|
|
697
|
+
context: workflowPaths.length ? { ...diagnostic.context, paths: workflowPaths } : diagnostic.context,
|
|
698
|
+
}));
|
|
699
|
+
const workflowDiagnostics = scopedDiagnostics.filter((diagnostic) => diagnostic.scope === "workflow");
|
|
700
|
+
const baselineDiagnostics = scopedDiagnostics.filter((diagnostic) => diagnostic.scope === "baseline");
|
|
527
701
|
return {
|
|
528
702
|
schemaVersion: 1,
|
|
529
703
|
repoName: context.repoName,
|
|
530
704
|
root: context.root,
|
|
531
705
|
strict,
|
|
532
|
-
status:
|
|
533
|
-
diagnostics,
|
|
706
|
+
status: scopedDiagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed",
|
|
707
|
+
diagnostics: scopedDiagnostics,
|
|
534
708
|
generatedFiles: context.generatedFiles,
|
|
535
|
-
generatedFilesFreshness,
|
|
709
|
+
generatedFilesFreshness: await generatedFreshnessFromStates(workspace),
|
|
536
710
|
validationCommands: context.validationCommands,
|
|
537
711
|
repairActions,
|
|
712
|
+
...(workflowPaths.length ? { baselineDiagnostics, workflowDiagnostics } : {}),
|
|
538
713
|
};
|
|
539
714
|
}
|
|
540
715
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.3.9-rc.
|
|
3
|
+
"version": "2.3.9-rc.3",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.3.9-rc.
|
|
35
|
+
"akanjs": "2.3.9-rc.3",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "5.5.23",
|
package/workflow/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capitalize } from "akanjs/common";
|
|
1
2
|
import type { Sys, Workspace } from "../commandDecorators";
|
|
2
3
|
import { AppExecutor, LibExecutor } from "../executors";
|
|
3
4
|
|
|
@@ -47,6 +48,7 @@ export interface WorkflowPredictedChange {
|
|
|
47
48
|
target: string;
|
|
48
49
|
action: "inspect" | "create" | "modify" | "sync" | "validate";
|
|
49
50
|
reason: string;
|
|
51
|
+
applyScope?: "auto" | "manual-review" | "generated-sync" | "validation";
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface WorkflowValidation {
|
|
@@ -76,6 +78,23 @@ export interface WorkflowDiagnostic {
|
|
|
76
78
|
command?: string;
|
|
77
79
|
kind?: WorkflowValidationKind;
|
|
78
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";
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
export interface WorkflowPlan {
|
|
@@ -88,6 +107,7 @@ export interface WorkflowPlan {
|
|
|
88
107
|
predictedChanges: readonly WorkflowPredictedChange[];
|
|
89
108
|
validation: readonly WorkflowValidation[];
|
|
90
109
|
diagnostics: WorkflowDiagnostic[];
|
|
110
|
+
recommendations: WorkflowRecommendation[];
|
|
91
111
|
requiresApproval: true;
|
|
92
112
|
}
|
|
93
113
|
|
|
@@ -167,6 +187,9 @@ export type PrimitiveFileMap = Record<string, { filename: string; content: strin
|
|
|
167
187
|
|
|
168
188
|
export interface WorkflowApplyReport {
|
|
169
189
|
schemaVersion: 1;
|
|
190
|
+
runId?: string;
|
|
191
|
+
applyReportPath?: string;
|
|
192
|
+
validationTarget?: string;
|
|
170
193
|
workflow: string;
|
|
171
194
|
mode: "dry-run" | "apply";
|
|
172
195
|
status: "passed" | "failed";
|
|
@@ -176,6 +199,7 @@ export interface WorkflowApplyReport {
|
|
|
176
199
|
recommendedValidationCommands: WorkflowApplyCommand[];
|
|
177
200
|
commands: WorkflowApplyCommand[];
|
|
178
201
|
diagnostics: WorkflowDiagnostic[];
|
|
202
|
+
recommendations: WorkflowRecommendation[];
|
|
179
203
|
nextActions: PrimitiveNextAction[];
|
|
180
204
|
plan: WorkflowPlan;
|
|
181
205
|
}
|
|
@@ -189,6 +213,8 @@ export interface WorkflowValidationRunReport {
|
|
|
189
213
|
status: "passed" | "failed";
|
|
190
214
|
commands: WorkflowValidationCommandResult[];
|
|
191
215
|
diagnostics: WorkflowDiagnostic[];
|
|
216
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
217
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
192
218
|
repairActions: RepairAction[];
|
|
193
219
|
nextActions: PrimitiveNextAction[];
|
|
194
220
|
plan?: WorkflowPlan;
|
|
@@ -196,6 +222,8 @@ export interface WorkflowValidationRunReport {
|
|
|
196
222
|
|
|
197
223
|
export interface RepairReport {
|
|
198
224
|
schemaVersion: 1;
|
|
225
|
+
runId?: string;
|
|
226
|
+
repairReportPath?: string;
|
|
199
227
|
command: string;
|
|
200
228
|
kind: RepairAction["kind"];
|
|
201
229
|
target: string | null;
|
|
@@ -204,6 +232,18 @@ export interface RepairReport {
|
|
|
204
232
|
repairActions: RepairAction[];
|
|
205
233
|
nextActions: PrimitiveNextAction[];
|
|
206
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[];
|
|
207
247
|
}
|
|
208
248
|
|
|
209
249
|
export type WorkflowRunArtifact = WorkflowApplyReport | WorkflowValidationRunReport | RepairReport;
|
|
@@ -213,6 +253,7 @@ export interface WorkflowStepResult {
|
|
|
213
253
|
generatedFiles?: PrimitiveGeneratedFile[];
|
|
214
254
|
commands?: WorkflowApplyCommand[];
|
|
215
255
|
diagnostics?: WorkflowDiagnostic[];
|
|
256
|
+
recommendations?: WorkflowRecommendation[];
|
|
216
257
|
nextActions?: PrimitiveNextAction[];
|
|
217
258
|
}
|
|
218
259
|
|
|
@@ -264,6 +305,92 @@ export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
|
|
|
264
305
|
export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
|
|
265
306
|
Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
|
|
266
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
|
+
|
|
267
394
|
export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string, unknown>): WorkflowPlan => {
|
|
268
395
|
const inputs: Record<string, WorkflowInputValue> = {};
|
|
269
396
|
const diagnostics: WorkflowDiagnostic[] = [];
|
|
@@ -328,6 +455,7 @@ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string,
|
|
|
328
455
|
predictedChanges: spec.predictedChanges,
|
|
329
456
|
validation: spec.validation,
|
|
330
457
|
diagnostics,
|
|
458
|
+
recommendations: createWorkflowPlanRecommendations(spec, inputs),
|
|
331
459
|
requiresApproval: true,
|
|
332
460
|
};
|
|
333
461
|
};
|
|
@@ -387,7 +515,10 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
|
387
515
|
...plan.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
|
|
388
516
|
"",
|
|
389
517
|
"## Predicted Changes",
|
|
390
|
-
...plan.predictedChanges.map((change) =>
|
|
518
|
+
...plan.predictedChanges.map((change) => {
|
|
519
|
+
const scope = change.applyScope ? ` (${change.applyScope})` : "";
|
|
520
|
+
return `- \`${change.action}\`${scope} ${change.target}: ${change.reason}`;
|
|
521
|
+
}),
|
|
391
522
|
"",
|
|
392
523
|
"## Validation",
|
|
393
524
|
...plan.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
|
|
@@ -397,6 +528,11 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
|
397
528
|
? plan.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
398
529
|
: ["- none"]),
|
|
399
530
|
"",
|
|
531
|
+
"## Recommendations",
|
|
532
|
+
...(plan.recommendations.length
|
|
533
|
+
? plan.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
|
|
534
|
+
: ["- none"]),
|
|
535
|
+
"",
|
|
400
536
|
].join("\n");
|
|
401
537
|
|
|
402
538
|
const workflowStatus = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
@@ -424,15 +560,25 @@ export const createWorkflowApplyReport = ({
|
|
|
424
560
|
recommendedValidationCommands,
|
|
425
561
|
commands = [],
|
|
426
562
|
diagnostics = [],
|
|
563
|
+
recommendations = [],
|
|
427
564
|
nextActions = [],
|
|
428
565
|
plan,
|
|
429
566
|
}: Omit<
|
|
430
567
|
WorkflowApplyReport,
|
|
431
|
-
|
|
568
|
+
| "schemaVersion"
|
|
569
|
+
| "runId"
|
|
570
|
+
| "applyReportPath"
|
|
571
|
+
| "validationTarget"
|
|
572
|
+
| "status"
|
|
573
|
+
| "appliedCommands"
|
|
574
|
+
| "recommendedValidationCommands"
|
|
575
|
+
| "commands"
|
|
576
|
+
| "recommendations"
|
|
432
577
|
> & {
|
|
433
578
|
appliedCommands?: WorkflowApplyCommand[];
|
|
434
579
|
recommendedValidationCommands?: WorkflowApplyCommand[];
|
|
435
580
|
commands?: WorkflowApplyCommand[];
|
|
581
|
+
recommendations?: WorkflowRecommendation[];
|
|
436
582
|
}): WorkflowApplyReport => {
|
|
437
583
|
const validationCommands = recommendedValidationCommands ?? commands;
|
|
438
584
|
return {
|
|
@@ -446,6 +592,7 @@ export const createWorkflowApplyReport = ({
|
|
|
446
592
|
recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
|
|
447
593
|
commands: uniqueBy(validationCommands, (command) => command.command),
|
|
448
594
|
diagnostics,
|
|
595
|
+
recommendations: uniqueBy(recommendations, (recommendation) => `${recommendation.kind}:${recommendation.code}`),
|
|
449
596
|
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
450
597
|
plan,
|
|
451
598
|
};
|
|
@@ -475,17 +622,55 @@ export const createWorkflowRunId = (prefix = "run") =>
|
|
|
475
622
|
.slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
476
623
|
|
|
477
624
|
const getRunId = (artifact: WorkflowRunArtifact) => {
|
|
478
|
-
if ("runId" in artifact) return artifact.runId;
|
|
625
|
+
if ("runId" in artifact && artifact.runId) return artifact.runId;
|
|
479
626
|
return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
|
|
480
627
|
};
|
|
481
628
|
|
|
482
629
|
export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
|
|
483
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
|
+
|
|
484
643
|
export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
|
|
485
644
|
const runId = getRunId(artifact);
|
|
486
645
|
const artifactPath = workflowRunArtifactPath(runId);
|
|
487
|
-
|
|
488
|
-
|
|
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;
|
|
489
674
|
};
|
|
490
675
|
|
|
491
676
|
export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
|
|
@@ -506,6 +691,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
506
691
|
commands,
|
|
507
692
|
execute,
|
|
508
693
|
diagnostics = [],
|
|
694
|
+
baselineDiagnostics = [],
|
|
695
|
+
workflowDiagnostics = [],
|
|
509
696
|
repairActions = [],
|
|
510
697
|
}: {
|
|
511
698
|
runId?: string;
|
|
@@ -515,6 +702,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
515
702
|
commands: WorkflowApplyCommand[];
|
|
516
703
|
execute: WorkflowValidationCommandExecutor;
|
|
517
704
|
diagnostics?: WorkflowDiagnostic[];
|
|
705
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
706
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
518
707
|
repairActions?: RepairAction[];
|
|
519
708
|
}): Promise<WorkflowValidationRunReport> => {
|
|
520
709
|
const results: WorkflowValidationCommandResult[] = [];
|
|
@@ -544,6 +733,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
544
733
|
status: workflowStatus([...diagnostics, ...commandDiagnostics]) === "failed" ? "failed" : commandStatus(results),
|
|
545
734
|
commands: results,
|
|
546
735
|
diagnostics: [...diagnostics, ...commandDiagnostics],
|
|
736
|
+
baselineDiagnostics,
|
|
737
|
+
workflowDiagnostics,
|
|
547
738
|
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
548
739
|
nextActions: results
|
|
549
740
|
.filter((result) => result.status === "failed")
|
|
@@ -581,6 +772,7 @@ export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
|
581
772
|
generatedFiles,
|
|
582
773
|
commands: workflowCommandsForPlan(plan),
|
|
583
774
|
diagnostics,
|
|
775
|
+
recommendations: plan.recommendations,
|
|
584
776
|
nextActions: workflowCommandsForPlan(plan),
|
|
585
777
|
plan,
|
|
586
778
|
});
|
|
@@ -593,6 +785,7 @@ export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport
|
|
|
593
785
|
generatedFiles: report.generatedFiles,
|
|
594
786
|
commands: report.validationCommands,
|
|
595
787
|
diagnostics: report.diagnostics,
|
|
788
|
+
recommendations: [],
|
|
596
789
|
nextActions: report.nextActions,
|
|
597
790
|
});
|
|
598
791
|
|
|
@@ -629,6 +822,31 @@ const unsupportedInput = (input: string, message: string): WorkflowDiagnostic =>
|
|
|
629
822
|
message,
|
|
630
823
|
});
|
|
631
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
|
+
|
|
632
850
|
export const createWorkflowStepRegistry = ({
|
|
633
851
|
workspace,
|
|
634
852
|
createModule,
|
|
@@ -699,7 +917,7 @@ export const createWorkflowStepRegistry = ({
|
|
|
699
917
|
);
|
|
700
918
|
},
|
|
701
919
|
[workflowStepKey("add-field", "update-dictionary")]: inspect,
|
|
702
|
-
[workflowStepKey("add-field", "update-ui-surfaces")]:
|
|
920
|
+
[workflowStepKey("add-field", "update-ui-surfaces")]: async (_step, plan) => addFieldUiSurfaceInspection(plan),
|
|
703
921
|
[workflowStepKey("add-enum-field", "update-constant")]: async (_step, plan) =>
|
|
704
922
|
primitiveReportToWorkflowStepResult(
|
|
705
923
|
await addEnumField({
|
|
@@ -756,6 +974,11 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
|
|
|
756
974
|
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
757
975
|
: ["- none"]),
|
|
758
976
|
"",
|
|
977
|
+
"## Recommendations",
|
|
978
|
+
...(report.recommendations.length
|
|
979
|
+
? report.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
|
|
980
|
+
: ["- none"]),
|
|
981
|
+
"",
|
|
759
982
|
"## Next Actions",
|
|
760
983
|
...(report.nextActions.length
|
|
761
984
|
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
@@ -818,12 +1041,25 @@ export const createRepairReport = ({
|
|
|
818
1041
|
repairActions = [],
|
|
819
1042
|
nextActions = [],
|
|
820
1043
|
commands = [],
|
|
1044
|
+
generatedFiles = [],
|
|
1045
|
+
syncedAt,
|
|
821
1046
|
}: Omit<
|
|
822
1047
|
RepairReport,
|
|
823
|
-
|
|
1048
|
+
| "schemaVersion"
|
|
1049
|
+
| "runId"
|
|
1050
|
+
| "repairReportPath"
|
|
1051
|
+
| "status"
|
|
1052
|
+
| "target"
|
|
1053
|
+
| "diagnostics"
|
|
1054
|
+
| "repairActions"
|
|
1055
|
+
| "nextActions"
|
|
1056
|
+
| "commands"
|
|
824
1057
|
> &
|
|
825
1058
|
Partial<
|
|
826
|
-
Pick<
|
|
1059
|
+
Pick<
|
|
1060
|
+
RepairReport,
|
|
1061
|
+
"target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
|
|
1062
|
+
>
|
|
827
1063
|
>): RepairReport => ({
|
|
828
1064
|
schemaVersion: 1,
|
|
829
1065
|
command,
|
|
@@ -834,6 +1070,8 @@ export const createRepairReport = ({
|
|
|
834
1070
|
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
835
1071
|
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
836
1072
|
commands,
|
|
1073
|
+
generatedFiles,
|
|
1074
|
+
...(syncedAt ? { syncedAt } : {}),
|
|
837
1075
|
});
|
|
838
1076
|
|
|
839
1077
|
export const renderRepairReportMarkdown = (report: RepairReport) =>
|
|
@@ -871,6 +1109,7 @@ export class WorkflowExecutor {
|
|
|
871
1109
|
const generatedFiles: PrimitiveGeneratedFile[] = [];
|
|
872
1110
|
const recommendedValidationCommands: WorkflowApplyCommand[] = [];
|
|
873
1111
|
const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
|
|
1112
|
+
const recommendations: WorkflowRecommendation[] = [...plan.recommendations];
|
|
874
1113
|
const nextActions: PrimitiveNextAction[] = [];
|
|
875
1114
|
|
|
876
1115
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
@@ -881,6 +1120,7 @@ export class WorkflowExecutor {
|
|
|
881
1120
|
generatedFiles,
|
|
882
1121
|
recommendedValidationCommands,
|
|
883
1122
|
diagnostics,
|
|
1123
|
+
recommendations,
|
|
884
1124
|
nextActions,
|
|
885
1125
|
plan,
|
|
886
1126
|
});
|
|
@@ -911,6 +1151,7 @@ export class WorkflowExecutor {
|
|
|
911
1151
|
generatedFiles.push(...(result.generatedFiles ?? []));
|
|
912
1152
|
recommendedValidationCommands.push(...(result.commands ?? []));
|
|
913
1153
|
diagnostics.push(...(result.diagnostics ?? []));
|
|
1154
|
+
recommendations.push(...(result.recommendations ?? []));
|
|
914
1155
|
nextActions.push(...(result.nextActions ?? []));
|
|
915
1156
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
|
|
916
1157
|
}
|
|
@@ -922,6 +1163,7 @@ export class WorkflowExecutor {
|
|
|
922
1163
|
generatedFiles,
|
|
923
1164
|
recommendedValidationCommands,
|
|
924
1165
|
diagnostics,
|
|
1166
|
+
recommendations,
|
|
925
1167
|
nextActions,
|
|
926
1168
|
plan,
|
|
927
1169
|
});
|
|
@@ -994,12 +1236,7 @@ export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile[
|
|
|
994
1236
|
});
|
|
995
1237
|
|
|
996
1238
|
export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
|
|
997
|
-
|
|
998
|
-
{ path: `${getSysRoot(sys)}/lib/cnst.ts`, action: "sync", reason },
|
|
999
|
-
{ path: `${getSysRoot(sys)}/lib/dict.ts`, action: "sync", reason },
|
|
1000
|
-
{ path: `${getSysRoot(sys)}/lib/option.ts`, action: "sync", reason },
|
|
1001
|
-
{ path: `${getSysRoot(sys)}/lib/index.ts`, action: "sync", reason },
|
|
1002
|
-
] satisfies PrimitiveGeneratedFile[];
|
|
1239
|
+
generatedFilePathsForTarget(getSysRoot(sys), reason);
|
|
1003
1240
|
|
|
1004
1241
|
export const validationCommandsForTarget = (target: string) =>
|
|
1005
1242
|
[
|