@akanjs/devkit 2.3.9-rc.1 → 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/executors.ts +5 -4
- package/package.json +2 -2
- package/workflow/index.ts +347 -37
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/executors.ts
CHANGED
|
@@ -503,7 +503,7 @@ export class Executor {
|
|
|
503
503
|
async writeFile(
|
|
504
504
|
filePath: string,
|
|
505
505
|
content: string | object,
|
|
506
|
-
{ overwrite = true }: { overwrite?: boolean } = {},
|
|
506
|
+
{ overwrite = true, silent = false }: { overwrite?: boolean; silent?: boolean } = {},
|
|
507
507
|
): Promise<FileContent> {
|
|
508
508
|
const writePath = this.getPath(filePath);
|
|
509
509
|
const dir = path.dirname(writePath);
|
|
@@ -521,7 +521,7 @@ export class Executor {
|
|
|
521
521
|
}
|
|
522
522
|
} else {
|
|
523
523
|
await FileSys.writeText(writePath, contentStr);
|
|
524
|
-
this.logger.rawLog(chalk.green(`File Create: ${filePath}`));
|
|
524
|
+
if (!silent) this.logger.rawLog(chalk.green(`File Create: ${filePath}`));
|
|
525
525
|
}
|
|
526
526
|
return { filePath: writePath, content: contentStr };
|
|
527
527
|
}
|
|
@@ -1357,14 +1357,15 @@ export class AppExecutor extends SysExecutor {
|
|
|
1357
1357
|
getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
|
|
1358
1358
|
const basePort = 8282;
|
|
1359
1359
|
const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
|
|
1360
|
-
const PORT =
|
|
1360
|
+
const PORT = (basePort + portOffset).toString();
|
|
1361
1361
|
const AKAN_PUBLIC_SERVER_PORT = portOffset ? (8282 + portOffset).toString() : undefined;
|
|
1362
1362
|
return {
|
|
1363
1363
|
...process.env,
|
|
1364
1364
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
1365
1365
|
AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
|
|
1366
1366
|
NODE_NO_WARNINGS: "1",
|
|
1367
|
-
|
|
1367
|
+
PORT,
|
|
1368
|
+
AKAN_PUBLIC_CLIENT_PORT: PORT,
|
|
1368
1369
|
...(AKAN_PUBLIC_SERVER_PORT ? { AKAN_PUBLIC_SERVER_PORT } : {}),
|
|
1369
1370
|
...env,
|
|
1370
1371
|
};
|
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
|
|
|
@@ -8,6 +9,8 @@ export type WorkflowFormat = "markdown" | "json";
|
|
|
8
9
|
export type WorkflowPlanInputs = Record<string, string | null>;
|
|
9
10
|
export type PrimitiveFormat = "markdown" | "json";
|
|
10
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";
|
|
11
14
|
|
|
12
15
|
export interface PrimitiveTargetInput {
|
|
13
16
|
app: string | null;
|
|
@@ -45,11 +48,13 @@ export interface WorkflowPredictedChange {
|
|
|
45
48
|
target: string;
|
|
46
49
|
action: "inspect" | "create" | "modify" | "sync" | "validate";
|
|
47
50
|
reason: string;
|
|
51
|
+
applyScope?: "auto" | "manual-review" | "generated-sync" | "validation";
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export interface WorkflowValidation {
|
|
51
55
|
command: string;
|
|
52
56
|
reason: string;
|
|
57
|
+
kind?: WorkflowValidationKind;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export interface WorkflowSpec {
|
|
@@ -70,6 +75,26 @@ export interface WorkflowDiagnostic {
|
|
|
70
75
|
code: string;
|
|
71
76
|
message: string;
|
|
72
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";
|
|
73
98
|
}
|
|
74
99
|
|
|
75
100
|
export interface WorkflowPlan {
|
|
@@ -82,6 +107,7 @@ export interface WorkflowPlan {
|
|
|
82
107
|
predictedChanges: readonly WorkflowPredictedChange[];
|
|
83
108
|
validation: readonly WorkflowValidation[];
|
|
84
109
|
diagnostics: WorkflowDiagnostic[];
|
|
110
|
+
recommendations: WorkflowRecommendation[];
|
|
85
111
|
requiresApproval: true;
|
|
86
112
|
}
|
|
87
113
|
|
|
@@ -98,6 +124,7 @@ export interface WorkflowApplyCommand {
|
|
|
98
124
|
command: string;
|
|
99
125
|
reason: string;
|
|
100
126
|
stepId?: string;
|
|
127
|
+
kind?: WorkflowValidationKind;
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
export type WorkflowRunSource =
|
|
@@ -108,8 +135,10 @@ export type WorkflowRunSource =
|
|
|
108
135
|
export interface WorkflowValidationCommandResult {
|
|
109
136
|
command: string;
|
|
110
137
|
reason: string;
|
|
138
|
+
kind?: WorkflowValidationKind;
|
|
111
139
|
status: "passed" | "failed";
|
|
112
140
|
exitCode: number;
|
|
141
|
+
failureScope?: WorkflowFailureScope;
|
|
113
142
|
stdout?: string;
|
|
114
143
|
stderr?: string;
|
|
115
144
|
}
|
|
@@ -158,13 +187,19 @@ export type PrimitiveFileMap = Record<string, { filename: string; content: strin
|
|
|
158
187
|
|
|
159
188
|
export interface WorkflowApplyReport {
|
|
160
189
|
schemaVersion: 1;
|
|
190
|
+
runId?: string;
|
|
191
|
+
applyReportPath?: string;
|
|
192
|
+
validationTarget?: string;
|
|
161
193
|
workflow: string;
|
|
162
194
|
mode: "dry-run" | "apply";
|
|
163
195
|
status: "passed" | "failed";
|
|
164
196
|
changedFiles: PrimitiveChangedFile[];
|
|
165
197
|
generatedFiles: PrimitiveGeneratedFile[];
|
|
198
|
+
appliedCommands: WorkflowApplyCommand[];
|
|
199
|
+
recommendedValidationCommands: WorkflowApplyCommand[];
|
|
166
200
|
commands: WorkflowApplyCommand[];
|
|
167
201
|
diagnostics: WorkflowDiagnostic[];
|
|
202
|
+
recommendations: WorkflowRecommendation[];
|
|
168
203
|
nextActions: PrimitiveNextAction[];
|
|
169
204
|
plan: WorkflowPlan;
|
|
170
205
|
}
|
|
@@ -178,6 +213,8 @@ export interface WorkflowValidationRunReport {
|
|
|
178
213
|
status: "passed" | "failed";
|
|
179
214
|
commands: WorkflowValidationCommandResult[];
|
|
180
215
|
diagnostics: WorkflowDiagnostic[];
|
|
216
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
217
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
181
218
|
repairActions: RepairAction[];
|
|
182
219
|
nextActions: PrimitiveNextAction[];
|
|
183
220
|
plan?: WorkflowPlan;
|
|
@@ -185,6 +222,8 @@ export interface WorkflowValidationRunReport {
|
|
|
185
222
|
|
|
186
223
|
export interface RepairReport {
|
|
187
224
|
schemaVersion: 1;
|
|
225
|
+
runId?: string;
|
|
226
|
+
repairReportPath?: string;
|
|
188
227
|
command: string;
|
|
189
228
|
kind: RepairAction["kind"];
|
|
190
229
|
target: string | null;
|
|
@@ -193,6 +232,18 @@ export interface RepairReport {
|
|
|
193
232
|
repairActions: RepairAction[];
|
|
194
233
|
nextActions: PrimitiveNextAction[];
|
|
195
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[];
|
|
196
247
|
}
|
|
197
248
|
|
|
198
249
|
export type WorkflowRunArtifact = WorkflowApplyReport | WorkflowValidationRunReport | RepairReport;
|
|
@@ -202,6 +253,7 @@ export interface WorkflowStepResult {
|
|
|
202
253
|
generatedFiles?: PrimitiveGeneratedFile[];
|
|
203
254
|
commands?: WorkflowApplyCommand[];
|
|
204
255
|
diagnostics?: WorkflowDiagnostic[];
|
|
256
|
+
recommendations?: WorkflowRecommendation[];
|
|
205
257
|
nextActions?: PrimitiveNextAction[];
|
|
206
258
|
}
|
|
207
259
|
|
|
@@ -253,6 +305,92 @@ export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
|
|
|
253
305
|
export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
|
|
254
306
|
Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
|
|
255
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
|
+
|
|
256
394
|
export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string, unknown>): WorkflowPlan => {
|
|
257
395
|
const inputs: Record<string, WorkflowInputValue> = {};
|
|
258
396
|
const diagnostics: WorkflowDiagnostic[] = [];
|
|
@@ -293,6 +431,20 @@ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string,
|
|
|
293
431
|
inputs[name] = value;
|
|
294
432
|
}
|
|
295
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
|
+
|
|
296
448
|
return {
|
|
297
449
|
schemaVersion: 1,
|
|
298
450
|
workflow: spec.name,
|
|
@@ -303,6 +455,7 @@ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string,
|
|
|
303
455
|
predictedChanges: spec.predictedChanges,
|
|
304
456
|
validation: spec.validation,
|
|
305
457
|
diagnostics,
|
|
458
|
+
recommendations: createWorkflowPlanRecommendations(spec, inputs),
|
|
306
459
|
requiresApproval: true,
|
|
307
460
|
};
|
|
308
461
|
};
|
|
@@ -362,7 +515,10 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
|
362
515
|
...plan.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
|
|
363
516
|
"",
|
|
364
517
|
"## Predicted Changes",
|
|
365
|
-
...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
|
+
}),
|
|
366
522
|
"",
|
|
367
523
|
"## Validation",
|
|
368
524
|
...plan.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
|
|
@@ -372,6 +528,11 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
|
372
528
|
? plan.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
373
529
|
: ["- none"]),
|
|
374
530
|
"",
|
|
531
|
+
"## Recommendations",
|
|
532
|
+
...(plan.recommendations.length
|
|
533
|
+
? plan.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
|
|
534
|
+
: ["- none"]),
|
|
535
|
+
"",
|
|
375
536
|
].join("\n");
|
|
376
537
|
|
|
377
538
|
const workflowStatus = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
@@ -395,22 +556,47 @@ export const createWorkflowApplyReport = ({
|
|
|
395
556
|
mode,
|
|
396
557
|
changedFiles = [],
|
|
397
558
|
generatedFiles = [],
|
|
559
|
+
appliedCommands = [],
|
|
560
|
+
recommendedValidationCommands,
|
|
398
561
|
commands = [],
|
|
399
562
|
diagnostics = [],
|
|
563
|
+
recommendations = [],
|
|
400
564
|
nextActions = [],
|
|
401
565
|
plan,
|
|
402
|
-
}: Omit<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
};
|
|
414
600
|
|
|
415
601
|
export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
|
|
416
602
|
const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
|
|
@@ -424,6 +610,7 @@ export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
|
|
|
424
610
|
plan.validation.map((validation) => ({
|
|
425
611
|
command: resolveWorkflowCommand(validation.command, plan),
|
|
426
612
|
reason: validation.reason,
|
|
613
|
+
kind: validation.kind,
|
|
427
614
|
})) satisfies WorkflowApplyCommand[];
|
|
428
615
|
|
|
429
616
|
export const workflowRunsDir = ".akan/workflows/runs";
|
|
@@ -435,17 +622,55 @@ export const createWorkflowRunId = (prefix = "run") =>
|
|
|
435
622
|
.slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
436
623
|
|
|
437
624
|
const getRunId = (artifact: WorkflowRunArtifact) => {
|
|
438
|
-
if ("runId" in artifact) return artifact.runId;
|
|
625
|
+
if ("runId" in artifact && artifact.runId) return artifact.runId;
|
|
439
626
|
return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
|
|
440
627
|
};
|
|
441
628
|
|
|
442
629
|
export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
|
|
443
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
|
+
|
|
444
643
|
export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
|
|
445
644
|
const runId = getRunId(artifact);
|
|
446
645
|
const artifactPath = workflowRunArtifactPath(runId);
|
|
447
|
-
|
|
448
|
-
|
|
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;
|
|
449
674
|
};
|
|
450
675
|
|
|
451
676
|
export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
|
|
@@ -466,6 +691,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
466
691
|
commands,
|
|
467
692
|
execute,
|
|
468
693
|
diagnostics = [],
|
|
694
|
+
baselineDiagnostics = [],
|
|
695
|
+
workflowDiagnostics = [],
|
|
469
696
|
repairActions = [],
|
|
470
697
|
}: {
|
|
471
698
|
runId?: string;
|
|
@@ -475,6 +702,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
475
702
|
commands: WorkflowApplyCommand[];
|
|
476
703
|
execute: WorkflowValidationCommandExecutor;
|
|
477
704
|
diagnostics?: WorkflowDiagnostic[];
|
|
705
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
706
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
478
707
|
repairActions?: RepairAction[];
|
|
479
708
|
}): Promise<WorkflowValidationRunReport> => {
|
|
480
709
|
const results: WorkflowValidationCommandResult[] = [];
|
|
@@ -488,6 +717,9 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
488
717
|
severity: "error" as const,
|
|
489
718
|
code: "workflow-validation-command-failed",
|
|
490
719
|
message: `Validation command failed: ${result.command}`,
|
|
720
|
+
command: result.command,
|
|
721
|
+
kind: result.kind,
|
|
722
|
+
failureScope: result.failureScope,
|
|
491
723
|
},
|
|
492
724
|
]
|
|
493
725
|
: [],
|
|
@@ -501,6 +733,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
501
733
|
status: workflowStatus([...diagnostics, ...commandDiagnostics]) === "failed" ? "failed" : commandStatus(results),
|
|
502
734
|
commands: results,
|
|
503
735
|
diagnostics: [...diagnostics, ...commandDiagnostics],
|
|
736
|
+
baselineDiagnostics,
|
|
737
|
+
workflowDiagnostics,
|
|
504
738
|
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
505
739
|
nextActions: results
|
|
506
740
|
.filter((result) => result.status === "failed")
|
|
@@ -538,6 +772,7 @@ export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
|
538
772
|
generatedFiles,
|
|
539
773
|
commands: workflowCommandsForPlan(plan),
|
|
540
774
|
diagnostics,
|
|
775
|
+
recommendations: plan.recommendations,
|
|
541
776
|
nextActions: workflowCommandsForPlan(plan),
|
|
542
777
|
plan,
|
|
543
778
|
});
|
|
@@ -550,6 +785,7 @@ export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport
|
|
|
550
785
|
generatedFiles: report.generatedFiles,
|
|
551
786
|
commands: report.validationCommands,
|
|
552
787
|
diagnostics: report.diagnostics,
|
|
788
|
+
recommendations: [],
|
|
553
789
|
nextActions: report.nextActions,
|
|
554
790
|
});
|
|
555
791
|
|
|
@@ -586,6 +822,31 @@ const unsupportedInput = (input: string, message: string): WorkflowDiagnostic =>
|
|
|
586
822
|
message,
|
|
587
823
|
});
|
|
588
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
|
+
|
|
589
850
|
export const createWorkflowStepRegistry = ({
|
|
590
851
|
workspace,
|
|
591
852
|
createModule,
|
|
@@ -656,7 +917,7 @@ export const createWorkflowStepRegistry = ({
|
|
|
656
917
|
);
|
|
657
918
|
},
|
|
658
919
|
[workflowStepKey("add-field", "update-dictionary")]: inspect,
|
|
659
|
-
[workflowStepKey("add-field", "update-ui-surfaces")]:
|
|
920
|
+
[workflowStepKey("add-field", "update-ui-surfaces")]: async (_step, plan) => addFieldUiSurfaceInspection(plan),
|
|
660
921
|
[workflowStepKey("add-enum-field", "update-constant")]: async (_step, plan) =>
|
|
661
922
|
primitiveReportToWorkflowStepResult(
|
|
662
923
|
await addEnumField({
|
|
@@ -698,9 +959,14 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
|
|
|
698
959
|
? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
699
960
|
: ["- none"]),
|
|
700
961
|
"",
|
|
701
|
-
"## Commands",
|
|
702
|
-
...(report.
|
|
703
|
-
? report.
|
|
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}`)
|
|
704
970
|
: ["- none"]),
|
|
705
971
|
"",
|
|
706
972
|
"## Diagnostics",
|
|
@@ -708,6 +974,11 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
|
|
|
708
974
|
? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
|
|
709
975
|
: ["- none"]),
|
|
710
976
|
"",
|
|
977
|
+
"## Recommendations",
|
|
978
|
+
...(report.recommendations.length
|
|
979
|
+
? report.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
|
|
980
|
+
: ["- none"]),
|
|
981
|
+
"",
|
|
711
982
|
"## Next Actions",
|
|
712
983
|
...(report.nextActions.length
|
|
713
984
|
? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
|
|
@@ -727,7 +998,10 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
|
|
|
727
998
|
"",
|
|
728
999
|
"## Commands",
|
|
729
1000
|
...(report.commands.length
|
|
730
|
-
? report.commands.map((command) =>
|
|
1001
|
+
? report.commands.map((command) => {
|
|
1002
|
+
const scope = command.failureScope ? ` (${command.failureScope})` : "";
|
|
1003
|
+
return `- [${command.status}] \`${command.command}\`${scope}: ${command.reason}`;
|
|
1004
|
+
})
|
|
731
1005
|
: ["- none"]),
|
|
732
1006
|
"",
|
|
733
1007
|
"## Diagnostics",
|
|
@@ -767,12 +1041,25 @@ export const createRepairReport = ({
|
|
|
767
1041
|
repairActions = [],
|
|
768
1042
|
nextActions = [],
|
|
769
1043
|
commands = [],
|
|
1044
|
+
generatedFiles = [],
|
|
1045
|
+
syncedAt,
|
|
770
1046
|
}: Omit<
|
|
771
1047
|
RepairReport,
|
|
772
|
-
|
|
1048
|
+
| "schemaVersion"
|
|
1049
|
+
| "runId"
|
|
1050
|
+
| "repairReportPath"
|
|
1051
|
+
| "status"
|
|
1052
|
+
| "target"
|
|
1053
|
+
| "diagnostics"
|
|
1054
|
+
| "repairActions"
|
|
1055
|
+
| "nextActions"
|
|
1056
|
+
| "commands"
|
|
773
1057
|
> &
|
|
774
1058
|
Partial<
|
|
775
|
-
Pick<
|
|
1059
|
+
Pick<
|
|
1060
|
+
RepairReport,
|
|
1061
|
+
"target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
|
|
1062
|
+
>
|
|
776
1063
|
>): RepairReport => ({
|
|
777
1064
|
schemaVersion: 1,
|
|
778
1065
|
command,
|
|
@@ -783,6 +1070,8 @@ export const createRepairReport = ({
|
|
|
783
1070
|
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
784
1071
|
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
785
1072
|
commands,
|
|
1073
|
+
generatedFiles,
|
|
1074
|
+
...(syncedAt ? { syncedAt } : {}),
|
|
786
1075
|
});
|
|
787
1076
|
|
|
788
1077
|
export const renderRepairReportMarkdown = (report: RepairReport) =>
|
|
@@ -818,8 +1107,9 @@ export class WorkflowExecutor {
|
|
|
818
1107
|
async apply(plan: WorkflowPlan): Promise<WorkflowApplyReport> {
|
|
819
1108
|
const changedFiles: PrimitiveChangedFile[] = [];
|
|
820
1109
|
const generatedFiles: PrimitiveGeneratedFile[] = [];
|
|
821
|
-
const
|
|
1110
|
+
const recommendedValidationCommands: WorkflowApplyCommand[] = [];
|
|
822
1111
|
const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
|
|
1112
|
+
const recommendations: WorkflowRecommendation[] = [...plan.recommendations];
|
|
823
1113
|
const nextActions: PrimitiveNextAction[] = [];
|
|
824
1114
|
|
|
825
1115
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
@@ -828,14 +1118,15 @@ export class WorkflowExecutor {
|
|
|
828
1118
|
mode: "apply",
|
|
829
1119
|
changedFiles,
|
|
830
1120
|
generatedFiles,
|
|
831
|
-
|
|
1121
|
+
recommendedValidationCommands,
|
|
832
1122
|
diagnostics,
|
|
1123
|
+
recommendations,
|
|
833
1124
|
nextActions,
|
|
834
1125
|
plan,
|
|
835
1126
|
});
|
|
836
1127
|
}
|
|
837
1128
|
|
|
838
|
-
|
|
1129
|
+
recommendedValidationCommands.push(...workflowCommandsForPlan(plan));
|
|
839
1130
|
nextActions.push(...workflowCommandsForPlan(plan));
|
|
840
1131
|
|
|
841
1132
|
for (const step of plan.steps) {
|
|
@@ -858,8 +1149,9 @@ export class WorkflowExecutor {
|
|
|
858
1149
|
if (!result) continue;
|
|
859
1150
|
changedFiles.push(...(result.changedFiles ?? []));
|
|
860
1151
|
generatedFiles.push(...(result.generatedFiles ?? []));
|
|
861
|
-
|
|
1152
|
+
recommendedValidationCommands.push(...(result.commands ?? []));
|
|
862
1153
|
diagnostics.push(...(result.diagnostics ?? []));
|
|
1154
|
+
recommendations.push(...(result.recommendations ?? []));
|
|
863
1155
|
nextActions.push(...(result.nextActions ?? []));
|
|
864
1156
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
|
|
865
1157
|
}
|
|
@@ -869,8 +1161,9 @@ export class WorkflowExecutor {
|
|
|
869
1161
|
mode: "apply",
|
|
870
1162
|
changedFiles,
|
|
871
1163
|
generatedFiles,
|
|
872
|
-
|
|
1164
|
+
recommendedValidationCommands,
|
|
873
1165
|
diagnostics,
|
|
1166
|
+
recommendations,
|
|
874
1167
|
nextActions,
|
|
875
1168
|
plan,
|
|
876
1169
|
});
|
|
@@ -943,12 +1236,7 @@ export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile[
|
|
|
943
1236
|
});
|
|
944
1237
|
|
|
945
1238
|
export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
|
|
946
|
-
|
|
947
|
-
{ path: `${getSysRoot(sys)}/lib/cnst.ts`, action: "sync", reason },
|
|
948
|
-
{ path: `${getSysRoot(sys)}/lib/dict.ts`, action: "sync", reason },
|
|
949
|
-
{ path: `${getSysRoot(sys)}/lib/option.ts`, action: "sync", reason },
|
|
950
|
-
{ path: `${getSysRoot(sys)}/lib/index.ts`, action: "sync", reason },
|
|
951
|
-
] satisfies PrimitiveGeneratedFile[];
|
|
1239
|
+
generatedFilePathsForTarget(getSysRoot(sys), reason);
|
|
952
1240
|
|
|
953
1241
|
export const validationCommandsForTarget = (target: string) =>
|
|
954
1242
|
[
|
|
@@ -1000,14 +1288,36 @@ export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}$
|
|
|
1000
1288
|
export const compactDiagnostics = (diagnostics: Array<WorkflowDiagnostic | false | null | undefined>) =>
|
|
1001
1289
|
diagnostics.filter((diagnostic): diagnostic is WorkflowDiagnostic => !!diagnostic);
|
|
1002
1290
|
|
|
1003
|
-
export const
|
|
1291
|
+
export const normalizeFieldType = (typeName: string) => {
|
|
1004
1292
|
const normalizedTypes: Record<string, string> = {
|
|
1005
1293
|
string: "String",
|
|
1006
|
-
number: "Number",
|
|
1007
1294
|
boolean: "Boolean",
|
|
1008
1295
|
date: "Date",
|
|
1296
|
+
int: "Int",
|
|
1297
|
+
integer: "Int",
|
|
1298
|
+
float: "Float",
|
|
1299
|
+
double: "Float",
|
|
1300
|
+
decimal: "Float",
|
|
1009
1301
|
};
|
|
1010
|
-
|
|
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);
|
|
1011
1321
|
const defaultOption = defaultValue ? `, { default: ${JSON.stringify(defaultValue)} }` : "";
|
|
1012
1322
|
return `field(${typeExpression}${defaultOption})`;
|
|
1013
1323
|
};
|