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

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.
@@ -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,14 +243,70 @@ 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; builderName?: string } = {},
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
- return `field(${typeExpression}${defaultOption})`;
302
+ return `${options.builderName ?? "field"}(${typeExpression}${defaultOption})`;
303
+ };
304
+
305
+ export const viaBuilderParameterName = (content: string, className: string) => {
306
+ const classIndex = content.indexOf(`export class ${className} extends via`);
307
+ if (classIndex < 0) return null;
308
+ const signature = content.slice(classIndex, content.indexOf("=>", classIndex));
309
+ return /via\(\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(signature)?.[1] ?? null;
148
310
  };
149
311
 
150
312
  export const insertIntoObject = (content: string, className: string, line: string) => {
@@ -158,6 +320,52 @@ export const insertIntoObject = (content: string, className: string, line: strin
158
320
  return `${prefix}${insertion}${suffix}`;
159
321
  };
160
322
 
323
+ export const insertLightProjectionField = (content: string, moduleClassName: string, fieldName: string) => {
324
+ const classIndex = content.indexOf(`export class Light${moduleClassName} extends via`);
325
+ if (classIndex < 0) return null;
326
+ const arrayMatch = /\[([\s\S]*?)\]\s+as const/.exec(content.slice(classIndex));
327
+ if (!arrayMatch || arrayMatch.index === undefined) return null;
328
+ const arrayStart = classIndex + arrayMatch.index;
329
+ const arrayEnd = arrayStart + arrayMatch[0].length;
330
+ const fields = [...arrayMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]).filter(Boolean);
331
+ if (fields.includes(fieldName)) return content;
332
+ const nextFields = [...fields, fieldName];
333
+ const nextArray =
334
+ nextFields.length === 0
335
+ ? "[] as const"
336
+ : `[\n${nextFields.map((field) => ` ${JSON.stringify(field)},`).join("\n")}\n] as const`;
337
+ return `${content.slice(0, arrayStart)}${nextArray}${content.slice(arrayEnd)}`;
338
+ };
339
+
340
+ export const insertTemplateField = ({
341
+ content,
342
+ moduleName,
343
+ moduleClassName,
344
+ fieldName,
345
+ component,
346
+ }: {
347
+ content: string;
348
+ moduleName: string;
349
+ moduleClassName: string;
350
+ fieldName: string;
351
+ component: "Field.Text" | "Field.Number" | "Field.Date";
352
+ }) => {
353
+ if (content.includes(`l("${moduleName}.${fieldName}")`) || content.includes(`.${fieldName}`)) return content;
354
+ const layoutEndIndex = content.indexOf(" </Layout.Template>");
355
+ if (layoutEndIndex < 0) return null;
356
+ const formName = `${moduleName}Form`;
357
+ if (!content.includes(`const ${formName} = st.use.${moduleName}Form();`)) return null;
358
+ const fieldSetter = `st.do.set${moduleComponentName(fieldName)}On${moduleClassName}`;
359
+ const fieldBlock = ` <${component}
360
+ label={l("${moduleName}.${fieldName}")}
361
+ desc={l("${moduleName}.${fieldName}.desc")}
362
+ value={${formName}.${fieldName}}
363
+ onChange={${fieldSetter}}
364
+ />
365
+ `;
366
+ return `${content.slice(0, layoutEndIndex)}${fieldBlock}${content.slice(layoutEndIndex)}`;
367
+ };
368
+
161
369
  export const ensureEnumImport = (content: string) => {
162
370
  if (content.includes("enumOf")) return content;
163
371
  const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
@@ -178,16 +386,60 @@ export const insertEnumClass = (content: string, enumClassName: string, enumName
178
386
  return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
179
387
  };
180
388
 
389
+ const findMatchingBrace = (content: string, openIndex: number) => {
390
+ let depth = 0;
391
+ let quote: '"' | "'" | "`" | null = null;
392
+ let escaped = false;
393
+ for (let index = openIndex; index < content.length; index++) {
394
+ const char = content[index];
395
+ if (quote) {
396
+ if (escaped) {
397
+ escaped = false;
398
+ continue;
399
+ }
400
+ if (char === "\\") {
401
+ escaped = true;
402
+ continue;
403
+ }
404
+ if (char === quote) quote = null;
405
+ continue;
406
+ }
407
+ if (char === '"' || char === "'" || char === "`") {
408
+ quote = char;
409
+ continue;
410
+ }
411
+ if (char === "{") depth += 1;
412
+ if (char === "}") {
413
+ depth -= 1;
414
+ if (depth === 0) return index;
415
+ }
416
+ }
417
+ return -1;
418
+ };
419
+
420
+ const dictionaryModelFieldLine = (fieldName: string) => {
421
+ const label = bilingualLabelForField(fieldName);
422
+ const desc = bilingualDescriptionForField(fieldName);
423
+ return `${fieldName}: t([${JSON.stringify(label.en)}, ${JSON.stringify(label.ko)}]).desc([${JSON.stringify(
424
+ desc.en,
425
+ )}, ${JSON.stringify(desc.ko)}]),`;
426
+ };
427
+
181
428
  export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
182
429
  if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
183
- const label = titleize(fieldName);
184
- const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
430
+ const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => (`);
185
431
  if (modelIndex < 0) return null;
186
- const objectEndIndex = content.indexOf(" }))", modelIndex);
432
+ const objectStartIndex = content.indexOf("{", modelIndex);
433
+ if (objectStartIndex < 0) return null;
434
+ const objectEndIndex = findMatchingBrace(content, objectStartIndex);
187
435
  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)}`;
436
+ const fieldLine = dictionaryModelFieldLine(fieldName);
437
+ const body = content.slice(objectStartIndex + 1, objectEndIndex);
438
+ if (body.trim().length === 0) {
439
+ return `${content.slice(0, objectStartIndex + 1)}\n ${fieldLine}\n ${content.slice(objectEndIndex)}`;
440
+ }
441
+ const insertion = body.endsWith("\n") ? ` ${fieldLine}\n` : `\n ${fieldLine}\n`;
442
+ return `${content.slice(0, objectEndIndex)}${insertion}${content.slice(objectEndIndex)}`;
191
443
  };
192
444
 
193
445
  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 {
@@ -165,6 +177,13 @@ export interface PrimitiveChangedFile {
165
177
  reason: string;
166
178
  }
167
179
 
180
+ export interface WorkflowPostApplyCheck {
181
+ code: string;
182
+ target: string;
183
+ status: "passed" | "failed";
184
+ message: string;
185
+ }
186
+
168
187
  export interface PrimitiveGeneratedFile {
169
188
  path: string;
170
189
  action: "sync";
@@ -208,6 +227,7 @@ export interface WorkflowApplyReport {
208
227
  recommendedValidationCommands: WorkflowApplyCommand[];
209
228
  commands: WorkflowApplyCommand[];
210
229
  diagnostics: WorkflowDiagnostic[];
230
+ postApplyChecks?: WorkflowPostApplyCheck[];
211
231
  recommendations: WorkflowRecommendation[];
212
232
  nextActions: PrimitiveNextAction[];
213
233
  plan: WorkflowPlan;
@@ -222,6 +242,7 @@ export interface WorkflowValidationRunReport {
222
242
  status: "passed" | "failed";
223
243
  sourceStatus: WorkflowValidationStatus;
224
244
  workspaceStatus: WorkflowValidationStatus;
245
+ summary: WorkflowValidationSummary;
225
246
  overallStatus: WorkflowOverallStatus;
226
247
  knownBlockers: WorkflowKnownBlocker[];
227
248
  commands: WorkflowValidationCommandResult[];
@@ -266,6 +287,7 @@ export interface WorkflowStepResult {
266
287
  generatedFiles?: PrimitiveGeneratedFile[];
267
288
  commands?: WorkflowApplyCommand[];
268
289
  diagnostics?: WorkflowDiagnostic[];
290
+ postApplyChecks?: WorkflowPostApplyCheck[];
269
291
  recommendations?: WorkflowRecommendation[];
270
292
  nextActions?: PrimitiveNextAction[];
271
293
  }
@@ -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
+ };