@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.
- package/devkitUtils.test.ts +2 -2
- package/executors.ts +17 -13
- package/getModelFileData.ts +5 -2
- package/package.json +2 -2
- package/workflow/artifacts.ts +20 -1
- package/workflow/executor.ts +16 -8
- package/workflow/index.ts +1 -0
- package/workflow/plan.ts +198 -33
- package/workflow/render.ts +48 -11
- package/workflow/source.ts +217 -15
- package/workflow/types.ts +17 -4
- package/workflow/uiPolicy.ts +45 -0
package/devkitUtils.test.ts
CHANGED
|
@@ -269,11 +269,11 @@ describe("getModelFileData", () => {
|
|
|
269
269
|
].join("\n"),
|
|
270
270
|
);
|
|
271
271
|
await write(
|
|
272
|
-
path.join(root, "apps/demo/lib/post/
|
|
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/
|
|
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]:
|
|
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]:
|
|
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]:
|
|
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]:
|
|
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:
|
|
830
|
+
results: unknown[]; // ESLint.LintResult[];
|
|
831
831
|
message: string;
|
|
832
|
-
errors:
|
|
833
|
-
warnings:
|
|
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) =>
|
|
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) =>
|
|
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
|
}
|
package/getModelFileData.ts
CHANGED
|
@@ -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}/${
|
|
21
|
-
const viewFilePath = `${modulePath}/lib/${modelName}/${
|
|
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.
|
|
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.
|
|
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",
|
package/workflow/artifacts.ts
CHANGED
|
@@ -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 {
|
|
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 ({
|
package/workflow/executor.ts
CHANGED
|
@@ -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
|
|
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}` : "*"}
|
|
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:
|
|
79
|
-
? `
|
|
80
|
-
: `
|
|
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:
|
|
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
package/workflow/plan.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { capitalize } from "akanjs/common";
|
|
2
|
-
import {
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
97
|
-
confidence:
|
|
98
|
-
action:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
111
|
-
action: `Review ${app}:${module} Template
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
367
|
+
optionalSurfaces: createWorkflowPlanOptionalSurfaces(spec, inputs),
|
|
203
368
|
steps: spec.steps,
|
|
204
|
-
predictedChanges: spec
|
|
369
|
+
predictedChanges: createWorkflowPlanPredictedChanges(spec, inputs),
|
|
205
370
|
validation: spec.validation,
|
|
206
371
|
diagnostics,
|
|
207
372
|
recommendations: createWorkflowPlanRecommendations(spec, inputs),
|
package/workflow/render.ts
CHANGED
|
@@ -84,19 +84,42 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
|
84
84
|
"",
|
|
85
85
|
].join("\n");
|
|
86
86
|
|
|
87
|
-
|
|
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
|
-
"##
|
|
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
|
|
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
|
-
|
|
185
|
+
const known = blocker.known ? " known" : "";
|
|
186
|
+
return `- [${blocker.failureScope}${known}]${command}${count}: ${blocker.message}`;
|
|
150
187
|
})
|
|
151
188
|
: ["- none"]),
|
|
152
189
|
"",
|
package/workflow/source.ts
CHANGED
|
@@ -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
|
-
|
|
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?:
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
128
|
-
if (
|
|
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
|
-
|
|
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 = (
|
|
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(
|
|
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 =
|
|
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(
|
|
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?:
|
|
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?:
|
|
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
|
+
};
|