@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 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 type { RepairAction } from "./workflow";
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 generatedFilesFreshness = {
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
- { strict = false }: { strict?: boolean } = {},
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: diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed",
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 = basePort ? (basePort + portOffset).toString() : undefined;
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
- ...(PORT ? { PORT, AKAN_PUBLIC_CLIENT_PORT: PORT } : {}),
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.1",
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.1",
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) => `- \`${change.action}\` ${change.target}: ${change.reason}`),
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<WorkflowApplyReport, "schemaVersion" | "status">): WorkflowApplyReport => ({
403
- schemaVersion: 1,
404
- workflow,
405
- mode,
406
- status: workflowStatus(diagnostics),
407
- changedFiles: uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
408
- generatedFiles: uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
409
- commands: uniqueBy(commands, (command) => command.command),
410
- diagnostics,
411
- nextActions: uniqueBy(nextActions, (action) => action.command),
412
- plan,
413
- });
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
- await workspace.writeFile(artifactPath, jsonText(artifact));
448
- return { runId, path: artifactPath };
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")]: inspect,
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.commands.length
703
- ? report.commands.map((command) => `- \`${command.command}\`: ${command.reason}`)
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) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
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
- "schemaVersion" | "status" | "target" | "diagnostics" | "repairActions" | "nextActions" | "commands"
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<RepairReport, "target" | "diagnostics" | "repairActions" | "nextActions" | "commands">
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 commands: WorkflowApplyCommand[] = [];
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
- commands,
1121
+ recommendedValidationCommands,
832
1122
  diagnostics,
1123
+ recommendations,
833
1124
  nextActions,
834
1125
  plan,
835
1126
  });
836
1127
  }
837
1128
 
838
- commands.push(...workflowCommandsForPlan(plan));
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
- commands.push(...(result.commands ?? []));
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
- commands,
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 fieldExpression = (typeName: string, defaultValue?: string | null) => {
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
- const typeExpression = normalizedTypes[typeName.toLowerCase()] ?? typeName;
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
  };