@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.
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,72 @@ 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
+ const applySourceStatus = (report: WorkflowApplyReport) =>
97
+ report.diagnostics.some(
98
+ (diagnostic) =>
99
+ diagnostic.severity === "error" &&
100
+ (diagnostic.failureScope === "source-change" ||
101
+ !diagnostic.failureScope ||
102
+ diagnostic.failureScope === "unknown"),
103
+ )
104
+ ? "failed"
105
+ : "passed";
106
+
107
+ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
108
+ const manualReviewItems = [
109
+ ...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
110
+ ...report.recommendations
111
+ .filter((recommendation) => recommendation.kind === "manual-action")
112
+ .map(renderRecommendation),
113
+ ];
114
+ const validationBlockers = report.diagnostics
115
+ .filter(
116
+ (diagnostic) =>
117
+ diagnostic.severity === "error" &&
118
+ (diagnostic.failureScope === "workspace-config" || diagnostic.failureScope === "environment"),
119
+ )
120
+ .map(renderDiagnostic);
121
+ const sourceBlockers = report.diagnostics
122
+ .filter(
123
+ (diagnostic) =>
124
+ diagnostic.severity === "error" &&
125
+ (diagnostic.failureScope === "source-change" ||
126
+ !diagnostic.failureScope ||
127
+ diagnostic.failureScope === "unknown"),
128
+ )
129
+ .map(renderDiagnostic);
130
+ return [
89
131
  `# Workflow Apply: ${report.workflow}`,
90
132
  "",
91
133
  `- Mode: ${report.mode}`,
92
134
  `- Status: ${report.status}`,
135
+ `- Source-change status: ${applySourceStatus(report)}`,
136
+ `- Workspace status: ${validationBlockers.length ? "blocked" : "not blocked during apply"}`,
137
+ ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
93
138
  "",
94
- "## Changed Files",
139
+ "## Apply Checks",
140
+ ...(report.postApplyChecks?.length
141
+ ? report.postApplyChecks.map((check) => `- [${check.status}] ${check.code} ${check.target}: ${check.message}`)
142
+ : ["- not run"]),
143
+ "",
144
+ "## Source Change Blockers",
145
+ ...(sourceBlockers.length ? sourceBlockers : ["- none"]),
146
+ "",
147
+ "## Automatically Modified",
95
148
  ...(report.changedFiles.length
96
149
  ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
97
150
  : ["- none"]),
98
151
  "",
99
- "## Generated Files",
152
+ "## Generated Sync",
100
153
  ...(report.generatedFiles.length
101
154
  ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
102
155
  : ["- none"]),
@@ -111,15 +164,21 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
111
164
  ? report.recommendedValidationCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
112
165
  : ["- none"]),
113
166
  "",
167
+ "## User Review Required",
168
+ ...(manualReviewItems.length ? manualReviewItems : ["- none"]),
169
+ "",
170
+ "## Validation Blockers",
171
+ ...(validationBlockers.length
172
+ ? validationBlockers
173
+ : report.recommendedValidationCommands.length
174
+ ? ["- none detected during apply; run validation with the validation target for command-level blockers."]
175
+ : ["- none"]),
176
+ "",
114
177
  "## Diagnostics",
115
- ...(report.diagnostics.length
116
- ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
117
- : ["- none"]),
178
+ ...(report.diagnostics.length ? report.diagnostics.map(renderDiagnostic) : ["- none"]),
118
179
  "",
119
180
  "## Recommendations",
120
- ...(report.recommendations.length
121
- ? report.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
122
- : ["- none"]),
181
+ ...(report.recommendations.length ? report.recommendations.slice(0, 3).map(renderRecommendation) : ["- none"]),
123
182
  "",
124
183
  "## Next Actions",
125
184
  ...(report.nextActions.length
@@ -127,6 +186,7 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
127
186
  : ["- none"]),
128
187
  "",
129
188
  ].join("\n");
189
+ };
130
190
 
131
191
  export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
132
192
  format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
@@ -141,12 +201,33 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
141
201
  `- Workspace status: ${report.workspaceStatus}`,
142
202
  `- Overall status: ${report.overallStatus}`,
143
203
  "",
204
+ "## Status Summary",
205
+ `- Source-change: ${report.summary.sourceChange}`,
206
+ `- Generated sync: ${report.summary.generatedSync}`,
207
+ `- Workspace config: ${report.summary.workspaceConfig}`,
208
+ `- Environment: ${report.summary.environment}`,
209
+ "",
210
+ "## Source Change Diagnostics",
211
+ ...(report.workflowDiagnostics?.length
212
+ ? report.workflowDiagnostics.map(
213
+ (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
214
+ )
215
+ : ["- none"]),
216
+ "",
217
+ "## Existing Workspace Blockers",
218
+ ...(report.baselineDiagnostics?.length
219
+ ? report.baselineDiagnostics.map(
220
+ (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
221
+ )
222
+ : ["- none"]),
223
+ "",
144
224
  "## Known Blockers",
145
225
  ...(report.knownBlockers.length
146
226
  ? report.knownBlockers.map((blocker) => {
147
227
  const command = blocker.command ? ` \`${blocker.command}\`` : "";
148
228
  const count = blocker.count > 1 ? ` (${blocker.count}x)` : "";
149
- return `- [${blocker.failureScope}]${command}${count}: ${blocker.message}`;
229
+ const known = blocker.known ? " known" : "";
230
+ return `- [${blocker.failureScope}${known}]${command}${count}: ${blocker.message}`;
150
231
  })
151
232
  : ["- none"]),
152
233
  "",