@akanjs/devkit 2.3.9-rc.4 → 2.3.9-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -269,11 +269,11 @@ describe("getModelFileData", () => {
269
269
  ].join("\n"),
270
270
  );
271
271
  await write(
272
- path.join(root, "apps/demo/lib/post/post.Unit.tsx"),
272
+ path.join(root, "apps/demo/lib/post/Post.Unit.tsx"),
273
273
  "export default function Unit() { return null; }\n",
274
274
  );
275
275
  await write(
276
- path.join(root, "apps/demo/lib/post/post.View.tsx"),
276
+ path.join(root, "apps/demo/lib/post/Post.View.tsx"),
277
277
  "export default function View() { return null; }\n",
278
278
  );
279
279
 
package/executors.ts CHANGED
@@ -626,14 +626,14 @@ export class Executor {
626
626
  overwrite?: boolean;
627
627
  },
628
628
  dict: { [key: string]: string } = {},
629
- options: { [key: string]: any } = {},
629
+ options: { [key: string]: unknown } = {},
630
630
  ): Promise<FileContent | null> {
631
631
  if (targetPath.endsWith(".ts") || targetPath.endsWith(".tsx")) {
632
632
  const getContent = (await import(templatePath)) as {
633
633
  default: (
634
634
  scanInfo: AppInfo | LibInfo | null,
635
635
  dict: { [key: string]: string },
636
- options?: { [key: string]: any },
636
+ options?: { [key: string]: unknown },
637
637
  ) => Promise<string | null | { filename: string; content: string }>;
638
638
  };
639
639
  const result = await getContent.default(scanInfo ?? null, dict, options);
@@ -686,7 +686,7 @@ export class Executor {
686
686
  template: string;
687
687
  scanInfo?: AppInfo | LibInfo | null;
688
688
  dict?: { [key: string]: string };
689
- options?: { [key: string]: any };
689
+ options?: { [key: string]: unknown };
690
690
  overwrite?: boolean;
691
691
  }): Promise<FileContent[]> {
692
692
  const templateRoot = await this.#resolveTemplateRoot();
@@ -752,7 +752,7 @@ export class Executor {
752
752
  basePath: string;
753
753
  template: string;
754
754
  dict?: { [key: string]: string };
755
- options?: { [key: string]: any };
755
+ options?: { [key: string]: unknown };
756
756
  overwrite?: boolean;
757
757
  }): Promise<FileContent[]> {
758
758
  const dict = {
@@ -827,10 +827,10 @@ export class Executor {
827
827
  filePath: string,
828
828
  { fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {},
829
829
  ): Promise<{
830
- results: any[]; // ESLint.LintResult[];
830
+ results: unknown[]; // ESLint.LintResult[];
831
831
  message: string;
832
- errors: any[]; // ESLintLinter.LintMessage[];
833
- warnings: any[]; // ESLintLinter.LintMessage[];
832
+ errors: unknown[]; // ESLintLinter.LintMessage[];
833
+ warnings: unknown[]; // ESLintLinter.LintMessage[];
834
834
  }> {
835
835
  const path = this.getPath(filePath);
836
836
  const linter = this.getLinter();
@@ -1225,40 +1225,44 @@ export class SysExecutor extends Executor {
1225
1225
  async getViewComponents() {
1226
1226
  const viewComponents = (await this.readdir("lib"))
1227
1227
  .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
1228
- .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.View.tsx`).exists());
1228
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.View.tsx`).exists());
1229
1229
  return viewComponents;
1230
1230
  }
1231
1231
 
1232
1232
  async getUnitComponents() {
1233
1233
  const unitComponents = (await this.readdir("lib"))
1234
1234
  .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
1235
- .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.Unit.tsx`).exists());
1235
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Unit.tsx`).exists());
1236
1236
  return unitComponents;
1237
1237
  }
1238
1238
  async getTemplateComponents() {
1239
1239
  const templateComponents = (await this.readdir("lib"))
1240
1240
  .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
1241
- .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.Template.tsx`).exists());
1241
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${capitalize(name)}.Template.tsx`).exists());
1242
1242
  return templateComponents;
1243
1243
  }
1244
1244
 
1245
1245
  async getViewsSourceCode() {
1246
1246
  const viewComponents = await this.getViewComponents();
1247
1247
  return Promise.all(
1248
- viewComponents.map((viewComponent) => this.getLocalFile(`lib/${viewComponent}/${viewComponent}.View.tsx`)),
1248
+ viewComponents.map((viewComponent) =>
1249
+ this.getLocalFile(`lib/${viewComponent}/${capitalize(viewComponent)}.View.tsx`),
1250
+ ),
1249
1251
  );
1250
1252
  }
1251
1253
  async getUnitsSourceCode() {
1252
1254
  const unitComponents = await this.getUnitComponents();
1253
1255
  return Promise.all(
1254
- unitComponents.map((unitComponent) => this.getLocalFile(`lib/${unitComponent}/${unitComponent}.Unit.tsx`)),
1256
+ unitComponents.map((unitComponent) =>
1257
+ this.getLocalFile(`lib/${unitComponent}/${capitalize(unitComponent)}.Unit.tsx`),
1258
+ ),
1255
1259
  );
1256
1260
  }
1257
1261
  async getTemplatesSourceCode() {
1258
1262
  const templateComponents = await this.getTemplateComponents();
1259
1263
  return Promise.all(
1260
1264
  templateComponents.map((templateComponent) =>
1261
- this.getLocalFile(`lib/${templateComponent}/${templateComponent}.Template.tsx`),
1265
+ this.getLocalFile(`lib/${templateComponent}/${capitalize(templateComponent)}.Template.tsx`),
1262
1266
  ),
1263
1267
  );
1264
1268
  }
@@ -1,3 +1,5 @@
1
+ import { capitalize } from "akanjs/common";
2
+
1
3
  interface ModelFileData {
2
4
  moduleType: "lib" | "app";
3
5
  moduleName: string;
@@ -16,9 +18,10 @@ interface ModelFileData {
16
18
  export const getModelFileData = async (modulePath: string, modelName: string): Promise<ModelFileData> => {
17
19
  const moduleType = modulePath.startsWith("apps") ? "app" : "lib";
18
20
  const moduleName = modulePath.split("/")[1] ?? "";
21
+ const modelComponentName = capitalize(modelName);
19
22
  const constantFilePath = `${modulePath}/lib/${modelName}/${modelName}.constant.ts`;
20
- const unitFilePath = `${modulePath}/lib/${modelName}/${modelName}.Unit.tsx`;
21
- const viewFilePath = `${modulePath}/lib/${modelName}/${modelName}.View.tsx`;
23
+ const unitFilePath = `${modulePath}/lib/${modelName}/${modelComponentName}.Unit.tsx`;
24
+ const viewFilePath = `${modulePath}/lib/${modelName}/${modelComponentName}.View.tsx`;
22
25
  const [constantFileStr, unitFileStr, viewFileStr] = await Promise.all([
23
26
  Bun.file(constantFilePath).text(),
24
27
  Bun.file(unitFilePath).text(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.3.9-rc.4",
3
+ "version": "2.3.9-rc.5",
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.4",
35
+ "akanjs": "2.3.9-rc.5",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "5.5.23",
@@ -186,6 +186,15 @@ const statusForScope = (
186
186
  return hasScopeFailure(scopes, expectedScope, diagnostics) ? "failed" : "passed";
187
187
  };
188
188
 
189
+ const statusForValidationKind = (
190
+ commands: readonly WorkflowValidationCommandResult[],
191
+ kind: WorkflowValidationCommandResult["kind"],
192
+ ): WorkflowValidationStatus => {
193
+ const matching = commands.filter((command) => command.kind === kind);
194
+ if (matching.length === 0) return "unknown";
195
+ return matching.some((command) => command.status === "failed") ? "failed" : "passed";
196
+ };
197
+
189
198
  const createKnownBlockers = (
190
199
  commands: readonly WorkflowValidationCommandResult[],
191
200
  diagnostics: readonly WorkflowDiagnostic[],
@@ -251,7 +260,17 @@ const createValidationStatuses = (
251
260
  : workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
252
261
  ? "failed"
253
262
  : "passed";
254
- return { sourceStatus, workspaceStatus, overallStatus };
263
+ return {
264
+ sourceStatus,
265
+ workspaceStatus,
266
+ overallStatus,
267
+ summary: {
268
+ sourceChange: sourceStatus,
269
+ generatedSync: statusForValidationKind(commands, "sync"),
270
+ workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
271
+ environment: statusForScope(commands, diagnostics, scopes, "environment"),
272
+ },
273
+ };
255
274
  };
256
275
 
257
276
  export const createWorkflowValidationRunReport = async ({
@@ -2,6 +2,7 @@ import { capitalize } from "akanjs/common";
2
2
  import type { Sys, Workspace } from "../commandDecorators";
3
3
  import { AppExecutor, LibExecutor } from "../executors";
4
4
  import { createWorkflowApplyReport, workflowCommandsForPlan } from "./artifacts";
5
+ import { moduleSourcePaths } from "./source";
5
6
  import type {
6
7
  PrimitiveChangedFile,
7
8
  PrimitiveGeneratedFile,
@@ -16,6 +17,7 @@ import type {
16
17
  WorkflowStepRegistry,
17
18
  WorkflowStepResult,
18
19
  } from "./types";
20
+ import { addFieldUiPolicyForType } from "./uiPolicy";
19
21
 
20
22
  export const workflowStepKey = (workflow: string, stepId: string) => `${workflow}:${stepId}`;
21
23
 
@@ -31,6 +33,8 @@ export const primitiveReportToWorkflowStepResult = (report: PrimitiveWriteReport
31
33
  const workflowStringInput = (value: WorkflowInputValue | undefined) => (typeof value === "string" ? value : null);
32
34
  const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
33
35
  Array.isArray(value) ? value.join(",") : null;
36
+ const workflowStringArrayInput = (value: WorkflowInputValue | undefined) => (Array.isArray(value) ? value : null);
37
+ const workflowBooleanInput = (value: WorkflowInputValue | undefined) => (typeof value === "boolean" ? value : null);
34
38
 
35
39
  const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
36
40
  if (!target) return null;
@@ -66,22 +70,22 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
66
70
  const module = workflowStringInput(plan.inputs.module) ?? "<module>";
67
71
  const field = workflowStringInput(plan.inputs.field) ?? "<field>";
68
72
  const typeName = workflowStringInput(plan.inputs.type);
69
- const isNumeric = typeName === "Int" || typeName === "Float" || typeName === "integer" || typeName === "float";
73
+ const policy = addFieldUiPolicyForType(typeName ?? "String");
74
+ const surfaces = workflowStringArrayInput(plan.inputs.surfaces);
75
+ const templateRequested = surfaces?.includes("template") ?? false;
70
76
  const moduleClassName = capitalize(module);
71
- const target = `${app ? `apps/${app}` : "*"}/lib/${module}/${moduleClassName}.Template.tsx`;
77
+ const target = `${app ? `apps/${app}` : "*"}/${moduleSourcePaths(module).template}`;
72
78
  return {
73
79
  recommendations: [
74
80
  {
75
81
  code: "add-field-ui-surface-review",
76
82
  kind: "manual-action",
77
83
  target,
78
- action: isNumeric
79
- ? `Review ${moduleClassName} Template/Unit/View/Store surfaces before adding ${field}; confirm the local Field.Text numeric parser/formatter, validation rule, and dictionary label pattern.`
80
- : `Add ${field} to ${moduleClassName} UI surfaces only when the local Field.* pattern is clear.`,
84
+ action: templateRequested
85
+ ? `Template was requested for ${field}. If no Template file changed, auto-edit was skipped because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Candidate position: inside Layout.Template near the existing Field components.`
86
+ : `Template was not selected, so UI files are intentionally left unchanged. Candidate positions if you expose it later: Layout.Template field list for editing, Light${moduleClassName} projection for list/card data, and Unit/View card sections for display.`,
81
87
  confidence: "medium",
82
- message: isNumeric
83
- ? `No UI file was modified automatically because a safe numeric input component pattern is not yet detected for ${module}.${field}.`
84
- : `Review Template/Unit/View/Store surfaces for ${module}.${field}; no UI file was modified automatically.`,
88
+ message: `Review UI surfaces for ${module}.${field}; recommended component is ${policy.component}, with manual reasons and candidate positions in action.`,
85
89
  },
86
90
  ],
87
91
  nextActions: [
@@ -149,6 +153,8 @@ export const createWorkflowStepRegistry = ({
149
153
  field: workflowStringInput(plan.inputs.field),
150
154
  values: workflowStringListInput(plan.inputs.values),
151
155
  defaultValue: workflowStringInput(plan.inputs.default),
156
+ surfaces: workflowStringArrayInput(plan.inputs.surfaces),
157
+ includeInLight: workflowBooleanInput(plan.inputs.includeInLight),
152
158
  }),
153
159
  );
154
160
  }
@@ -159,6 +165,8 @@ export const createWorkflowStepRegistry = ({
159
165
  field: workflowStringInput(plan.inputs.field),
160
166
  type: workflowStringInput(plan.inputs.type),
161
167
  defaultValue: workflowStringInput(plan.inputs.default),
168
+ surfaces: workflowStringArrayInput(plan.inputs.surfaces),
169
+ includeInLight: workflowBooleanInput(plan.inputs.includeInLight),
162
170
  }),
163
171
  );
164
172
  },
package/workflow/index.ts CHANGED
@@ -5,4 +5,5 @@ export * from "./primitive";
5
5
  export * from "./render";
6
6
  export * from "./source";
7
7
  export * from "./types";
8
+ export * from "./uiPolicy";
8
9
  export * from "./utils";
package/workflow/plan.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { capitalize } from "akanjs/common";
2
- import { normalizeFieldType } from "./source";
2
+ import { coerceFieldDefault, moduleSourcePaths } from "./source";
3
3
  import type {
4
4
  WorkflowDiagnostic,
5
5
  WorkflowInputSpec,
@@ -10,6 +10,7 @@ import type {
10
10
  WorkflowSpec,
11
11
  WorkflowSurfaceMode,
12
12
  } from "./types";
13
+ import { addFieldUiPolicyForType } from "./uiPolicy";
13
14
 
14
15
  const surfaceModes = new Set<WorkflowSurfaceMode>(["infer", "include", "skip"]);
15
16
 
@@ -31,6 +32,14 @@ const normalizeInputValue = (name: string, spec: WorkflowInputSpec, value: unkno
31
32
  const values = parseStringList(value);
32
33
  return values && values.length > 0 ? values : null;
33
34
  }
35
+ if (spec.type === "boolean") {
36
+ if (typeof value === "boolean") return value;
37
+ if (typeof value !== "string") return null;
38
+ const lowered = value.trim().toLowerCase();
39
+ if (lowered === "true") return true;
40
+ if (lowered === "false") return false;
41
+ return null;
42
+ }
34
43
  if (typeof value === "string" && surfaceModes.has(value as WorkflowSurfaceMode)) return value as WorkflowSurfaceMode;
35
44
  throw new Error(`Unsupported workflow input value for ${name}`);
36
45
  };
@@ -44,13 +53,47 @@ export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
44
53
  export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
45
54
  Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
46
55
 
47
- const addFieldComponentForType = (typeName: string) => {
48
- const normalizedType = normalizeFieldType(typeName);
49
- if (normalizedType === "Boolean") return "Field.ToggleSelect";
50
- if (normalizedType === "Date") return "Field.Date";
51
- if (normalizedType === "Int" || normalizedType === "Float") return "manual numeric input review";
52
- if (typeName.toLowerCase() === "enum") return "Field.ToggleSelect";
53
- return "Field.Text";
56
+ const addFieldTargetRoot = (app: string | null) => (app ? `apps/${app}` : "*");
57
+
58
+ const addFieldPaths = (inputs: Record<string, WorkflowInputValue>) => {
59
+ const app = typeof inputs.app === "string" ? inputs.app : null;
60
+ const module = typeof inputs.module === "string" ? inputs.module : "<module>";
61
+ const paths = moduleSourcePaths(module);
62
+ const root = addFieldTargetRoot(app);
63
+ return {
64
+ constant: `${root}/${paths.constant}`,
65
+ dictionary: `${root}/${paths.dictionary}`,
66
+ template: `${root}/${paths.template}`,
67
+ unit: `${root}/${paths.unit}`,
68
+ view: `${root}/${paths.view}`,
69
+ };
70
+ };
71
+
72
+ const selectedAddFieldSurfaces = (inputs: Record<string, WorkflowInputValue>) =>
73
+ Array.isArray(inputs.surfaces) ? new Set(inputs.surfaces) : null;
74
+
75
+ const addFieldDefaultCoercion = (inputs: Record<string, WorkflowInputValue>) => {
76
+ const typeName = typeof inputs.type === "string" ? inputs.type : null;
77
+ const defaultValue = typeof inputs.default === "string" ? inputs.default : null;
78
+ if (!typeName || defaultValue === null) return null;
79
+ if (typeName.toLowerCase() === "number" || typeName.toLowerCase() === "numeric") return null;
80
+ const enumValues = Array.isArray(inputs.values) ? inputs.values : null;
81
+ return coerceFieldDefault(typeName.toLowerCase() === "enum" ? "enum" : typeName, defaultValue, { enumValues });
82
+ };
83
+
84
+ const createAddFieldDefaultRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
85
+ const field = typeof inputs.field === "string" ? inputs.field : "<field>";
86
+ const coercion = addFieldDefaultCoercion(inputs);
87
+ if (!coercion?.normalized || !coercion.expression) return [];
88
+ return [
89
+ {
90
+ code: "add-field-default-normalized",
91
+ kind: "manual-action",
92
+ confidence: "high",
93
+ message: `Default for ${field} will be normalized to ${coercion.normalizedType} literal ${coercion.expression}.`,
94
+ action: "Review the normalized default in the apply report if this value should stay unset.",
95
+ },
96
+ ];
54
97
  };
55
98
 
56
99
  const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
@@ -60,17 +103,19 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
60
103
  const typeName = typeof inputs.type === "string" ? inputs.type : null;
61
104
  if (!typeName) return [];
62
105
 
63
- const normalizedType = typeName.toLowerCase() === "enum" ? "enum" : normalizeFieldType(typeName);
64
- const constantPath = `*/lib/${module}/${module}.constant.ts`;
65
- const dictionaryPath = `*/lib/${module}/${module}.dictionary.ts`;
66
- const templatePath = `*/lib/${module}/${capitalize(module)}.Template.tsx`;
106
+ const policy = addFieldUiPolicyForType(typeName);
107
+ const normalizedType = policy.normalizedType;
108
+ const paths = addFieldPaths(inputs);
109
+ const surfaces = selectedAddFieldSurfaces(inputs);
110
+ const templateRequested = surfaces?.has("template") ?? false;
111
+ const includeInLight = inputs.includeInLight === true;
67
112
  return [
68
113
  ...(normalizedType === "Int" || normalizedType === "Float"
69
114
  ? [
70
115
  {
71
116
  code: "add-field-import",
72
117
  kind: "import" as const,
73
- target: constantPath,
118
+ target: paths.constant,
74
119
  confidence: "high" as const,
75
120
  message: `Import ${normalizedType} from "akanjs/base" before writing field(${normalizedType}).`,
76
121
  },
@@ -79,45 +124,147 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
79
124
  {
80
125
  code: "add-field-placement-constant",
81
126
  kind: "placement",
82
- target: constantPath,
127
+ target: paths.constant,
83
128
  confidence: "high",
84
129
  message: `Insert ${field}: field(${normalizedType}) in ${capitalize(module)}Input.`,
85
130
  },
86
131
  {
87
132
  code: "add-field-placement-dictionary",
88
133
  kind: "placement",
89
- target: dictionaryPath,
134
+ target: paths.dictionary,
90
135
  confidence: "high",
91
136
  message: `Add dictionary labels for ${module}.${field}.`,
92
137
  },
93
138
  {
94
139
  code: "add-field-component",
95
140
  kind: "ui-component",
96
- target: templatePath,
97
- confidence: normalizedType === "Int" || normalizedType === "Float" ? "low" : "high",
98
- action:
99
- normalizedType === "Int" || normalizedType === "Float"
100
- ? "Do not auto-edit UI. Check local Template/Unit/View patterns for Field.Text parsing, formatting, validation rules, and dictionary labels before adding a numeric input."
101
- : undefined,
102
- message:
103
- normalizedType === "Int" || normalizedType === "Float"
104
- ? `Numeric UI for ${field} (${normalizedType}) requires manual review; a safe numeric input component pattern is not yet detected.`
105
- : `Recommended UI component for ${field} (${normalizedType}): ${addFieldComponentForType(typeName)}.`,
141
+ target: paths.template,
142
+ confidence: policy.confidence,
143
+ action: templateRequested
144
+ ? `apply_workflow will try to add ${policy.component} to Template when the existing Template has a safe generated field-list pattern.`
145
+ : `Recommended component is ${policy.component}. Pass surfaces=["template"] when this field should be rendered in the Template form.`,
146
+ message: `Recommended UI component for ${field} (${normalizedType}): ${policy.component}.`,
106
147
  },
148
+ ...(!surfaces
149
+ ? [
150
+ {
151
+ code: "add-field-template-surface-choice",
152
+ kind: "manual-action" as const,
153
+ target: paths.template,
154
+ action: `Choose whether to expose ${field} in Template by passing surfaces=["template"].`,
155
+ confidence: "medium" as const,
156
+ message: `Template exposure for ${module}.${field} is not selected yet.`,
157
+ },
158
+ ]
159
+ : []),
160
+ ...(!includeInLight
161
+ ? [
162
+ {
163
+ code: "add-field-light-projection-choice",
164
+ kind: "manual-action" as const,
165
+ target: paths.constant,
166
+ action: `Pass includeInLight=true when ${field} should appear in Light${capitalize(module)} list/card projections.`,
167
+ confidence: "medium" as const,
168
+ message: `Light projection exposure for ${module}.${field} is not selected yet.`,
169
+ },
170
+ ]
171
+ : []),
172
+ ...(includeInLight
173
+ ? [
174
+ {
175
+ code: "add-field-light-projection",
176
+ kind: "placement" as const,
177
+ target: paths.constant,
178
+ confidence: "high" as const,
179
+ message: `Add ${field} to Light${capitalize(module)} projection fields.`,
180
+ },
181
+ ]
182
+ : []),
183
+ ...(surfaces && !templateRequested
184
+ ? [
185
+ {
186
+ code: "add-field-ui-surface-skip",
187
+ kind: "manual-action" as const,
188
+ target: paths.template,
189
+ confidence: "medium" as const,
190
+ message: `Template is not included in surfaces, so ${module}.${field} will not be auto-rendered there.`,
191
+ },
192
+ ]
193
+ : []),
107
194
  {
108
195
  code: "add-field-ui-manual-review",
109
196
  kind: "manual-action",
110
- target: templatePath,
111
- action: `Review ${app}:${module} Template/Unit/View/Store surfaces and add ${field} only where the existing pattern is clear. For numeric fields, confirm parser/formatter behavior and validation before using Field.Text.`,
197
+ target: paths.template,
198
+ action: `Review ${app}:${module} UI after apply. Template auto-edit requires a generated ${module}Form hook and Layout.Template field list; Unit/View are not auto-edited because list/card placement depends on local layout. Candidate positions: Layout.Template before the closing tag, Light${capitalize(
199
+ module,
200
+ )} projection array for list data, and Unit/View card sections for display.`,
112
201
  confidence: "medium",
113
- message:
114
- normalizedType === "Int" || normalizedType === "Float"
115
- ? "UI surface edits stay manual because a safe numeric input component pattern is not yet detected."
116
- : "UI surface edits are planned as manual-review unless a safe existing field-list pattern is detected.",
202
+ message: "UI manual review includes the reason auto-edit may be skipped and candidate insertion positions.",
203
+ },
204
+ ...createAddFieldDefaultRecommendations(inputs),
205
+ ];
206
+ };
207
+
208
+ const createWorkflowPlanPredictedChanges = (
209
+ spec: WorkflowSpec,
210
+ inputs: Record<string, WorkflowInputValue>,
211
+ ): WorkflowPlan["predictedChanges"] => {
212
+ if (spec.name !== "add-field") return spec.predictedChanges;
213
+ const paths = addFieldPaths(inputs);
214
+ const surfaces = selectedAddFieldSurfaces(inputs);
215
+ const includeInLight = inputs.includeInLight === true;
216
+ return [
217
+ {
218
+ target: paths.constant,
219
+ action: "modify",
220
+ applyScope: "auto",
221
+ reason: includeInLight ? "Field shape and Light projection are added." : "Field shape is added.",
222
+ },
223
+ {
224
+ target: paths.dictionary,
225
+ action: "modify",
226
+ applyScope: "auto",
227
+ reason: "Field label is added.",
228
+ },
229
+ ...(!surfaces || surfaces.has("template")
230
+ ? [
231
+ {
232
+ target: paths.template,
233
+ action: "modify" as const,
234
+ applyScope: surfaces?.has("template") ? ("auto" as const) : ("manual-review" as const),
235
+ reason: surfaces?.has("template")
236
+ ? "Template form is selected for safe-pattern auto insertion."
237
+ : "Form surface may include the field.",
238
+ },
239
+ ]
240
+ : []),
241
+ {
242
+ target: `${addFieldTargetRoot(typeof inputs.app === "string" ? inputs.app : null)}/lib/cnst.ts`,
243
+ action: "sync",
244
+ applyScope: "generated-sync",
245
+ reason: "Generated constants may change after sync.",
246
+ },
247
+ {
248
+ target: `${addFieldTargetRoot(typeof inputs.app === "string" ? inputs.app : null)}/lib/dict.ts`,
249
+ action: "sync",
250
+ applyScope: "generated-sync",
251
+ reason: "Generated dictionary barrel may change after sync.",
117
252
  },
118
253
  ];
119
254
  };
120
255
 
256
+ const createWorkflowPlanOptionalSurfaces = (
257
+ spec: WorkflowSpec,
258
+ inputs: Record<string, WorkflowInputValue>,
259
+ ): Record<string, WorkflowSurfaceMode> => {
260
+ const optionalSurfaces = spec.optionalSurfaces ?? {};
261
+ const surfaces = selectedAddFieldSurfaces(inputs);
262
+ if (spec.name !== "add-field" || !surfaces) return optionalSurfaces;
263
+ return Object.fromEntries(
264
+ Object.entries(optionalSurfaces).map(([surface, mode]) => [surface, surfaces.has(surface) ? "include" : mode]),
265
+ );
266
+ };
267
+
121
268
  const createWorkflowPlanRecommendations = (
122
269
  spec: WorkflowSpec,
123
270
  inputs: Record<string, WorkflowInputValue>,
@@ -193,15 +340,33 @@ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string,
193
340
  message: `Field type "${fieldType}" is ambiguous in Akan. Use Int for integer fields or Float for decimal fields.`,
194
341
  });
195
342
  }
343
+ if (spec.name === "add-field") {
344
+ const defaultCoercion = addFieldDefaultCoercion(inputs);
345
+ if (defaultCoercion?.diagnostic) {
346
+ diagnostics.push({
347
+ ...defaultCoercion.diagnostic,
348
+ code: "workflow-default-value-invalid",
349
+ message: `Plan input default is not valid for ${defaultCoercion.normalizedType}: ${defaultCoercion.diagnostic.message}`,
350
+ });
351
+ } else if (defaultCoercion?.normalized && defaultCoercion.expression) {
352
+ diagnostics.push({
353
+ severity: "warning",
354
+ code: "workflow-default-value-normalized",
355
+ input: "default",
356
+ failureScope: "source-change",
357
+ message: `Plan input default will be written as ${defaultCoercion.normalizedType} literal ${defaultCoercion.expression}.`,
358
+ });
359
+ }
360
+ }
196
361
 
197
362
  return {
198
363
  schemaVersion: 1,
199
364
  workflow: spec.name,
200
365
  mode: "plan",
201
366
  inputs,
202
- optionalSurfaces: spec.optionalSurfaces ?? {},
367
+ optionalSurfaces: createWorkflowPlanOptionalSurfaces(spec, inputs),
203
368
  steps: spec.steps,
204
- predictedChanges: spec.predictedChanges,
369
+ predictedChanges: createWorkflowPlanPredictedChanges(spec, inputs),
205
370
  validation: spec.validation,
206
371
  diagnostics,
207
372
  recommendations: createWorkflowPlanRecommendations(spec, inputs),
@@ -84,19 +84,42 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
84
84
  "",
85
85
  ].join("\n");
86
86
 
87
- export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
88
- [
87
+ const renderRecommendation = (recommendation: WorkflowApplyReport["recommendations"][number]) => {
88
+ const target = recommendation.target ? ` ${recommendation.target}` : "";
89
+ const action = recommendation.action ? ` Action: ${recommendation.action}` : "";
90
+ return `- [${recommendation.kind}]${target} ${recommendation.message}${action}`;
91
+ };
92
+
93
+ const renderDiagnostic = (diagnostic: WorkflowApplyReport["diagnostics"][number]) =>
94
+ `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`;
95
+
96
+ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
97
+ const manualReviewItems = [
98
+ ...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
99
+ ...report.recommendations
100
+ .filter((recommendation) => recommendation.kind === "manual-action")
101
+ .map(renderRecommendation),
102
+ ];
103
+ const validationBlockers = report.diagnostics
104
+ .filter(
105
+ (diagnostic) =>
106
+ diagnostic.severity === "error" &&
107
+ (diagnostic.failureScope === "workspace-config" || diagnostic.failureScope === "environment"),
108
+ )
109
+ .map(renderDiagnostic);
110
+ return [
89
111
  `# Workflow Apply: ${report.workflow}`,
90
112
  "",
91
113
  `- Mode: ${report.mode}`,
92
114
  `- Status: ${report.status}`,
115
+ ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
93
116
  "",
94
- "## Changed Files",
117
+ "## Automatically Modified",
95
118
  ...(report.changedFiles.length
96
119
  ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
97
120
  : ["- none"]),
98
121
  "",
99
- "## Generated Files",
122
+ "## Generated Sync",
100
123
  ...(report.generatedFiles.length
101
124
  ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
102
125
  : ["- none"]),
@@ -111,15 +134,21 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
111
134
  ? report.recommendedValidationCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
112
135
  : ["- none"]),
113
136
  "",
137
+ "## User Review Required",
138
+ ...(manualReviewItems.length ? manualReviewItems : ["- none"]),
139
+ "",
140
+ "## Validation Blockers",
141
+ ...(validationBlockers.length
142
+ ? validationBlockers
143
+ : report.recommendedValidationCommands.length
144
+ ? ["- none detected during apply; run validation with the validation target for command-level blockers."]
145
+ : ["- none"]),
146
+ "",
114
147
  "## Diagnostics",
115
- ...(report.diagnostics.length
116
- ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
117
- : ["- none"]),
148
+ ...(report.diagnostics.length ? report.diagnostics.map(renderDiagnostic) : ["- none"]),
118
149
  "",
119
150
  "## Recommendations",
120
- ...(report.recommendations.length
121
- ? report.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
122
- : ["- none"]),
151
+ ...(report.recommendations.length ? report.recommendations.map(renderRecommendation) : ["- none"]),
123
152
  "",
124
153
  "## Next Actions",
125
154
  ...(report.nextActions.length
@@ -127,6 +156,7 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
127
156
  : ["- none"]),
128
157
  "",
129
158
  ].join("\n");
159
+ };
130
160
 
131
161
  export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
132
162
  format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
@@ -141,12 +171,19 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
141
171
  `- Workspace status: ${report.workspaceStatus}`,
142
172
  `- Overall status: ${report.overallStatus}`,
143
173
  "",
174
+ "## Status Summary",
175
+ `- Required source-change validation: ${report.summary.sourceChange}`,
176
+ `- Generated sync validation: ${report.summary.generatedSync}`,
177
+ `- Workspace configuration validation: ${report.summary.workspaceConfig}`,
178
+ `- Environment validation: ${report.summary.environment}`,
179
+ "",
144
180
  "## Known Blockers",
145
181
  ...(report.knownBlockers.length
146
182
  ? report.knownBlockers.map((blocker) => {
147
183
  const command = blocker.command ? ` \`${blocker.command}\`` : "";
148
184
  const count = blocker.count > 1 ? ` (${blocker.count}x)` : "";
149
- return `- [${blocker.failureScope}]${command}${count}: ${blocker.message}`;
185
+ const known = blocker.known ? " known" : "";
186
+ return `- [${blocker.failureScope}${known}]${command}${count}: ${blocker.message}`;
150
187
  })
151
188
  : ["- none"]),
152
189
  "",
@@ -18,6 +18,26 @@ export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile[
18
18
  reason,
19
19
  });
20
20
 
21
+ export const moduleComponentName = (moduleName: string) =>
22
+ `${moduleName.slice(0, 1).toUpperCase()}${moduleName.slice(1)}`;
23
+
24
+ export const moduleSourcePaths = (moduleName: string) => {
25
+ const componentName = moduleComponentName(moduleName);
26
+ return {
27
+ abstract: `lib/${moduleName}/${moduleName}.abstract.md`,
28
+ constant: `lib/${moduleName}/${moduleName}.constant.ts`,
29
+ dictionary: `lib/${moduleName}/${moduleName}.dictionary.ts`,
30
+ service: `lib/${moduleName}/${moduleName}.service.ts`,
31
+ signal: `lib/${moduleName}/${moduleName}.signal.ts`,
32
+ store: `lib/${moduleName}/${moduleName}.store.ts`,
33
+ template: `lib/${moduleName}/${componentName}.Template.tsx`,
34
+ unit: `lib/${moduleName}/${componentName}.Unit.tsx`,
35
+ util: `lib/${moduleName}/${componentName}.Util.tsx`,
36
+ view: `lib/${moduleName}/${componentName}.View.tsx`,
37
+ zone: `lib/${moduleName}/${componentName}.Zone.tsx`,
38
+ };
39
+ };
40
+
21
41
  export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
22
42
  generatedFilePathsForTarget(getSysRoot(sys), reason);
23
43
 
@@ -68,6 +88,60 @@ export const titleize = (value: string) =>
68
88
 
69
89
  export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
70
90
 
91
+ const koLabels: Record<string, string> = {
92
+ amount: "금액",
93
+ budget: "예산",
94
+ category: "카테고리",
95
+ content: "내용",
96
+ count: "개수",
97
+ createdAt: "생성일",
98
+ date: "날짜",
99
+ description: "설명",
100
+ due: "마감일",
101
+ dueAt: "마감일",
102
+ email: "이메일",
103
+ enabled: "활성화",
104
+ endAt: "종료일",
105
+ id: "ID",
106
+ name: "이름",
107
+ owner: "담당자",
108
+ priority: "우선순위",
109
+ project: "프로젝트",
110
+ rating: "평점",
111
+ startAt: "시작일",
112
+ status: "상태",
113
+ title: "제목",
114
+ updatedAt: "수정일",
115
+ };
116
+
117
+ const splitFieldWords = (fieldName: string) =>
118
+ fieldName
119
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
120
+ .replace(/[-_]+/g, " ")
121
+ .split(/\s+/)
122
+ .map((word) => word.trim())
123
+ .filter(Boolean);
124
+
125
+ const koLabelForField = (fieldName: string) => {
126
+ if (koLabels[fieldName]) return koLabels[fieldName];
127
+ const words = splitFieldWords(fieldName);
128
+ const translated = words.map((word) => koLabels[word] ?? koLabels[lowerlize(word)] ?? null);
129
+ return translated.every(Boolean) ? translated.join(" ") : null;
130
+ };
131
+
132
+ export const bilingualLabelForField = (fieldName: string) => {
133
+ const en = titleize(fieldName);
134
+ return { en, ko: koLabelForField(fieldName) ?? en };
135
+ };
136
+
137
+ export const bilingualDescriptionForField = (fieldName: string) => {
138
+ const label = bilingualLabelForField(fieldName);
139
+ return {
140
+ en: `Enter ${label.en.toLowerCase()}.`,
141
+ ko: `${label.ko} 값을 입력합니다.`,
142
+ };
143
+ };
144
+
71
145
  export const normalizeFieldType = (typeName: string) => {
72
146
  const normalizedTypes: Record<string, string> = {
73
147
  string: "String",
@@ -96,24 +170,54 @@ export const ensureBaseTypeImport = (content: string, typeName: string) => {
96
170
  return `import { ${typeName} } from "akanjs/base";\n${content}`;
97
171
  };
98
172
 
99
- const numericDefault = (typeName: "Int" | "Float", rawDefault: string): string | null => {
173
+ export type FieldDefaultValue = string | number | boolean | null;
174
+
175
+ const numericDefault = (typeName: "Int" | "Float", rawDefault: FieldDefaultValue): string | null => {
176
+ if (typeof rawDefault === "number") {
177
+ if (!Number.isFinite(rawDefault)) return null;
178
+ if (typeName === "Int" && !Number.isInteger(rawDefault)) return null;
179
+ return String(rawDefault);
180
+ }
181
+ if (typeof rawDefault !== "string") return null;
100
182
  const trimmed = rawDefault.trim();
101
183
  if (!trimmed || !Number.isFinite(Number(trimmed))) return null;
102
184
  if (typeName === "Int" && !/^-?\d+$/.test(trimmed)) return null;
103
185
  return trimmed;
104
186
  };
105
187
 
188
+ const booleanDefault = (rawDefault: FieldDefaultValue): string | null => {
189
+ if (typeof rawDefault === "boolean") return String(rawDefault);
190
+ if (typeof rawDefault !== "string") return null;
191
+ const lowered = rawDefault.trim().toLowerCase();
192
+ return lowered === "true" || lowered === "false" ? lowered : null;
193
+ };
194
+
195
+ const dateDefault = (rawDefault: FieldDefaultValue): string | null => {
196
+ if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `new Date(${rawDefault})`;
197
+ if (typeof rawDefault !== "string") return null;
198
+ const trimmed = rawDefault.trim();
199
+ if (!trimmed) return null;
200
+ if (trimmed === "now") return "new Date()";
201
+ if (!Number.isNaN(Date.parse(trimmed))) return `new Date(${JSON.stringify(trimmed)})`;
202
+ return null;
203
+ };
204
+
106
205
  export const coerceFieldDefault = (
107
206
  typeName: string,
108
- defaultValue?: string | null,
109
- ): { expression: string | null; diagnostic?: WorkflowDiagnostic } => {
110
- if (defaultValue === undefined || defaultValue === null || defaultValue === "") return { expression: null };
111
- const normalizedType = normalizeFieldType(typeName);
207
+ defaultValue?: FieldDefaultValue,
208
+ options: { enumValues?: readonly string[] | null } = {},
209
+ ): { expression: string | null; diagnostic?: WorkflowDiagnostic; normalized: boolean; normalizedType: string } => {
210
+ const normalizedType = typeName.toLowerCase() === "enum" ? "enum" : normalizeFieldType(typeName);
211
+ if (defaultValue === undefined || defaultValue === null || defaultValue === "") {
212
+ return { expression: null, normalized: false, normalizedType };
213
+ }
112
214
  if (normalizedType === "Int" || normalizedType === "Float") {
113
215
  const expression = numericDefault(normalizedType, defaultValue);
114
- if (expression !== null) return { expression };
216
+ if (expression !== null) return { expression, normalized: typeof defaultValue === "string", normalizedType };
115
217
  return {
116
218
  expression: null,
219
+ normalized: false,
220
+ normalizedType,
117
221
  diagnostic: {
118
222
  severity: "error",
119
223
  code: "primitive-default-value-invalid",
@@ -124,10 +228,12 @@ export const coerceFieldDefault = (
124
228
  };
125
229
  }
126
230
  if (normalizedType === "Boolean") {
127
- const lowered = defaultValue.trim().toLowerCase();
128
- if (lowered === "true" || lowered === "false") return { expression: lowered };
231
+ const expression = booleanDefault(defaultValue);
232
+ if (expression !== null) return { expression, normalized: typeof defaultValue === "string", normalizedType };
129
233
  return {
130
234
  expression: null,
235
+ normalized: false,
236
+ normalizedType,
131
237
  diagnostic: {
132
238
  severity: "error",
133
239
  code: "primitive-default-value-invalid",
@@ -137,12 +243,61 @@ export const coerceFieldDefault = (
137
243
  },
138
244
  };
139
245
  }
140
- return { expression: JSON.stringify(defaultValue) };
246
+ if (normalizedType === "Date") {
247
+ const expression = dateDefault(defaultValue);
248
+ if (expression !== null) return { expression, normalized: true, normalizedType };
249
+ return {
250
+ expression: null,
251
+ normalized: false,
252
+ normalizedType,
253
+ diagnostic: {
254
+ severity: "error",
255
+ code: "primitive-default-value-invalid",
256
+ input: "default",
257
+ failureScope: "source-change",
258
+ message: `Default value for Date must be "now", a timestamp, or a parseable date string. Received: ${JSON.stringify(
259
+ defaultValue,
260
+ )}.`,
261
+ },
262
+ };
263
+ }
264
+ if (normalizedType === "enum") {
265
+ if (typeof defaultValue === "string" && options.enumValues?.includes(defaultValue)) {
266
+ return { expression: JSON.stringify(defaultValue), normalized: false, normalizedType };
267
+ }
268
+ return {
269
+ expression: null,
270
+ normalized: false,
271
+ normalizedType,
272
+ diagnostic: {
273
+ severity: "error",
274
+ code: "primitive-default-value-invalid",
275
+ input: "default",
276
+ failureScope: "source-change",
277
+ message: `Default value for enum must be one of: ${(options.enumValues ?? []).join(", ")}. Received: ${JSON.stringify(
278
+ defaultValue,
279
+ )}.`,
280
+ },
281
+ };
282
+ }
283
+ return {
284
+ expression: JSON.stringify(String(defaultValue)),
285
+ normalized: typeof defaultValue !== "string",
286
+ normalizedType,
287
+ };
141
288
  };
142
289
 
143
- export const fieldExpression = (typeName: string, defaultValue?: string | null) => {
290
+ export const fieldExpression = (
291
+ typeName: string,
292
+ defaultValue?: FieldDefaultValue,
293
+ options: { enumValues?: readonly string[] | null } = {},
294
+ ) => {
144
295
  const typeExpression = normalizeFieldType(typeName);
145
- const defaultExpression = coerceFieldDefault(typeExpression, defaultValue).expression;
296
+ const defaultExpression = coerceFieldDefault(
297
+ options.enumValues ? "enum" : typeExpression,
298
+ defaultValue,
299
+ options,
300
+ ).expression;
146
301
  const defaultOption = defaultExpression ? `, { default: ${defaultExpression} }` : "";
147
302
  return `field(${typeExpression}${defaultOption})`;
148
303
  };
@@ -158,6 +313,52 @@ export const insertIntoObject = (content: string, className: string, line: strin
158
313
  return `${prefix}${insertion}${suffix}`;
159
314
  };
160
315
 
316
+ export const insertLightProjectionField = (content: string, moduleClassName: string, fieldName: string) => {
317
+ const classIndex = content.indexOf(`export class Light${moduleClassName} extends via`);
318
+ if (classIndex < 0) return null;
319
+ const arrayMatch = /\[([\s\S]*?)\]\s+as const/.exec(content.slice(classIndex));
320
+ if (!arrayMatch || arrayMatch.index === undefined) return null;
321
+ const arrayStart = classIndex + arrayMatch.index;
322
+ const arrayEnd = arrayStart + arrayMatch[0].length;
323
+ const fields = [...arrayMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]).filter(Boolean);
324
+ if (fields.includes(fieldName)) return content;
325
+ const nextFields = [...fields, fieldName];
326
+ const nextArray =
327
+ nextFields.length === 0
328
+ ? "[] as const"
329
+ : `[\n${nextFields.map((field) => ` ${JSON.stringify(field)},`).join("\n")}\n] as const`;
330
+ return `${content.slice(0, arrayStart)}${nextArray}${content.slice(arrayEnd)}`;
331
+ };
332
+
333
+ export const insertTemplateField = ({
334
+ content,
335
+ moduleName,
336
+ moduleClassName,
337
+ fieldName,
338
+ component,
339
+ }: {
340
+ content: string;
341
+ moduleName: string;
342
+ moduleClassName: string;
343
+ fieldName: string;
344
+ component: "Field.Text" | "Field.Number" | "Field.Date";
345
+ }) => {
346
+ if (content.includes(`l("${moduleName}.${fieldName}")`) || content.includes(`.${fieldName}`)) return content;
347
+ const layoutEndIndex = content.indexOf(" </Layout.Template>");
348
+ if (layoutEndIndex < 0) return null;
349
+ const formName = `${moduleName}Form`;
350
+ if (!content.includes(`const ${formName} = st.use.${moduleName}Form();`)) return null;
351
+ const fieldSetter = `st.do.set${moduleComponentName(fieldName)}On${moduleClassName}`;
352
+ const fieldBlock = ` <${component}
353
+ label={l("${moduleName}.${fieldName}")}
354
+ desc={l("${moduleName}.${fieldName}.desc")}
355
+ value={${formName}.${fieldName}}
356
+ onChange={${fieldSetter}}
357
+ />
358
+ `;
359
+ return `${content.slice(0, layoutEndIndex)}${fieldBlock}${content.slice(layoutEndIndex)}`;
360
+ };
361
+
161
362
  export const ensureEnumImport = (content: string) => {
162
363
  if (content.includes("enumOf")) return content;
163
364
  const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
@@ -180,14 +381,15 @@ export const insertEnumClass = (content: string, enumClassName: string, enumName
180
381
 
181
382
  export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
182
383
  if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
183
- const label = titleize(fieldName);
384
+ const label = bilingualLabelForField(fieldName);
385
+ const desc = bilingualDescriptionForField(fieldName);
184
386
  const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
185
387
  if (modelIndex < 0) return null;
186
388
  const objectEndIndex = content.indexOf(" }))", modelIndex);
187
389
  if (objectEndIndex < 0) return null;
188
- return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label)}, ${JSON.stringify(
189
- label,
190
- )}]).desc([${JSON.stringify(label)}, ${JSON.stringify(label)}]),\n${content.slice(objectEndIndex)}`;
390
+ return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label.en)}, ${JSON.stringify(
391
+ label.ko,
392
+ )}]).desc([${JSON.stringify(desc.en)}, ${JSON.stringify(desc.ko)}]),\n${content.slice(objectEndIndex)}`;
191
393
  };
192
394
 
193
395
  export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
package/workflow/types.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Sys, Workspace } from "../commandDecorators";
2
+ import type { FieldDefaultValue } from "./source";
2
3
 
3
- export type WorkflowInputType = "string" | "string-list" | "surface-mode";
4
+ export type WorkflowInputType = "string" | "string-list" | "surface-mode" | "boolean";
4
5
  export type WorkflowSurfaceMode = "infer" | "include" | "skip";
5
- export type WorkflowInputValue = string | string[] | WorkflowSurfaceMode;
6
+ export type WorkflowInputValue = string | string[] | boolean | WorkflowSurfaceMode;
6
7
  export type WorkflowFormat = "markdown" | "json";
7
8
  export type WorkflowPlanInputs = Record<string, string | null>;
8
9
  export type PrimitiveFormat = "markdown" | "json";
@@ -10,6 +11,12 @@ export type UiSurface = "view" | "unit" | "template";
10
11
  export type WorkflowValidationKind = "sync" | "lint" | "typecheck" | "doctor" | "custom";
11
12
  export type WorkflowFailureScope = "workspace-config" | "environment" | "source-change" | "unknown";
12
13
  export type WorkflowValidationStatus = "passed" | "failed" | "unknown";
14
+ export interface WorkflowValidationSummary {
15
+ sourceChange: WorkflowValidationStatus;
16
+ generatedSync: WorkflowValidationStatus;
17
+ workspaceConfig: WorkflowValidationStatus;
18
+ environment: WorkflowValidationStatus;
19
+ }
13
20
  export type WorkflowOverallStatus = "passed" | "failed" | "blocked-by-workspace-config" | "blocked-by-environment";
14
21
 
15
22
  export interface PrimitiveTargetInput {
@@ -20,13 +27,17 @@ export interface PrimitiveTargetInput {
20
27
  export interface AddFieldInput extends PrimitiveTargetInput {
21
28
  field: string | null;
22
29
  type: string | null;
23
- defaultValue?: string | null;
30
+ defaultValue?: FieldDefaultValue;
31
+ surfaces?: string[] | null;
32
+ includeInLight?: boolean | null;
24
33
  }
25
34
 
26
35
  export interface AddEnumFieldInput extends PrimitiveTargetInput {
27
36
  field: string | null;
28
37
  values: string | null;
29
- defaultValue?: string | null;
38
+ defaultValue?: FieldDefaultValue;
39
+ surfaces?: string[] | null;
40
+ includeInLight?: boolean | null;
30
41
  }
31
42
 
32
43
  export interface WorkflowInputSpec {
@@ -150,6 +161,7 @@ export interface WorkflowKnownBlocker {
150
161
  command?: string;
151
162
  kind?: WorkflowValidationKind;
152
163
  count: number;
164
+ known?: boolean;
153
165
  }
154
166
 
155
167
  export interface RepairAction {
@@ -222,6 +234,7 @@ export interface WorkflowValidationRunReport {
222
234
  status: "passed" | "failed";
223
235
  sourceStatus: WorkflowValidationStatus;
224
236
  workspaceStatus: WorkflowValidationStatus;
237
+ summary: WorkflowValidationSummary;
225
238
  overallStatus: WorkflowOverallStatus;
226
239
  knownBlockers: WorkflowKnownBlocker[];
227
240
  commands: WorkflowValidationCommandResult[];
@@ -0,0 +1,45 @@
1
+ import { normalizeFieldType } from "./source";
2
+
3
+ export type AddFieldUiComponent = "Field.Text" | "Field.Number" | "Field.Date" | "Field.ToggleSelect";
4
+
5
+ export const addFieldUiPolicyForType = (typeName: string) => {
6
+ const normalizedType = typeName.toLowerCase() === "enum" ? "enum" : normalizeFieldType(typeName);
7
+ if (normalizedType === "Int" || normalizedType === "Float") {
8
+ return {
9
+ normalizedType,
10
+ component: "Field.Number" as const,
11
+ confidence: "high" as const,
12
+ autoTemplateSupported: true,
13
+ };
14
+ }
15
+ if (normalizedType === "Date") {
16
+ return {
17
+ normalizedType,
18
+ component: "Field.Date" as const,
19
+ confidence: "high" as const,
20
+ autoTemplateSupported: true,
21
+ };
22
+ }
23
+ if (normalizedType === "Boolean" || normalizedType === "enum") {
24
+ return {
25
+ normalizedType,
26
+ component: "Field.ToggleSelect" as const,
27
+ confidence: "medium" as const,
28
+ autoTemplateSupported: false,
29
+ };
30
+ }
31
+ if (normalizedType === "String") {
32
+ return {
33
+ normalizedType,
34
+ component: "Field.Text" as const,
35
+ confidence: "high" as const,
36
+ autoTemplateSupported: true,
37
+ };
38
+ }
39
+ return {
40
+ normalizedType,
41
+ component: "Field.Text" as const,
42
+ confidence: "low" as const,
43
+ autoTemplateSupported: false,
44
+ };
45
+ };