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

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.
@@ -0,0 +1,210 @@
1
+ import { capitalize } from "akanjs/common";
2
+ import { normalizeFieldType } from "./source";
3
+ import type {
4
+ WorkflowDiagnostic,
5
+ WorkflowInputSpec,
6
+ WorkflowInputValue,
7
+ WorkflowPlan,
8
+ WorkflowPlanInputs,
9
+ WorkflowRecommendation,
10
+ WorkflowSpec,
11
+ WorkflowSurfaceMode,
12
+ } from "./types";
13
+
14
+ const surfaceModes = new Set<WorkflowSurfaceMode>(["infer", "include", "skip"]);
15
+
16
+ const parseStringList = (value: unknown) => {
17
+ if (Array.isArray(value)) {
18
+ const values = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
19
+ return values.length === value.length ? values : null;
20
+ }
21
+ if (typeof value !== "string") return null;
22
+ return value
23
+ .split(",")
24
+ .map((item) => item.trim())
25
+ .filter(Boolean);
26
+ };
27
+
28
+ const normalizeInputValue = (name: string, spec: WorkflowInputSpec, value: unknown): WorkflowInputValue | null => {
29
+ if (spec.type === "string") return typeof value === "string" && value.length > 0 ? value : null;
30
+ if (spec.type === "string-list") {
31
+ const values = parseStringList(value);
32
+ return values && values.length > 0 ? values : null;
33
+ }
34
+ if (typeof value === "string" && surfaceModes.has(value as WorkflowSurfaceMode)) return value as WorkflowSurfaceMode;
35
+ throw new Error(`Unsupported workflow input value for ${name}`);
36
+ };
37
+
38
+ export const listWorkflowSpecs = (specs: readonly WorkflowSpec[]) =>
39
+ [...specs].sort((a, b) => a.name.localeCompare(b.name));
40
+
41
+ export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
42
+ specs.find((spec) => spec.name === name) ?? null;
43
+
44
+ export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
45
+ Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
46
+
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";
54
+ };
55
+
56
+ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
57
+ const app = typeof inputs.app === "string" ? inputs.app : "<app-or-lib>";
58
+ const module = typeof inputs.module === "string" ? inputs.module : "<module>";
59
+ const field = typeof inputs.field === "string" ? inputs.field : "<field>";
60
+ const typeName = typeof inputs.type === "string" ? inputs.type : null;
61
+ if (!typeName) return [];
62
+
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`;
67
+ return [
68
+ ...(normalizedType === "Int" || normalizedType === "Float"
69
+ ? [
70
+ {
71
+ code: "add-field-import",
72
+ kind: "import" as const,
73
+ target: constantPath,
74
+ confidence: "high" as const,
75
+ message: `Import ${normalizedType} from "akanjs/base" before writing field(${normalizedType}).`,
76
+ },
77
+ ]
78
+ : []),
79
+ {
80
+ code: "add-field-placement-constant",
81
+ kind: "placement",
82
+ target: constantPath,
83
+ confidence: "high",
84
+ message: `Insert ${field}: field(${normalizedType}) in ${capitalize(module)}Input.`,
85
+ },
86
+ {
87
+ code: "add-field-placement-dictionary",
88
+ kind: "placement",
89
+ target: dictionaryPath,
90
+ confidence: "high",
91
+ message: `Add dictionary labels for ${module}.${field}.`,
92
+ },
93
+ {
94
+ code: "add-field-component",
95
+ 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)}.`,
106
+ },
107
+ {
108
+ code: "add-field-ui-manual-review",
109
+ 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.`,
112
+ 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.",
117
+ },
118
+ ];
119
+ };
120
+
121
+ const createWorkflowPlanRecommendations = (
122
+ spec: WorkflowSpec,
123
+ inputs: Record<string, WorkflowInputValue>,
124
+ ): WorkflowRecommendation[] => [
125
+ {
126
+ code: "workflow-apply-first",
127
+ kind: "auto-apply",
128
+ confidence: "high",
129
+ action: "Call apply_workflow with the MCP planPath before editing source files directly.",
130
+ message: `Apply the ${spec.name} plan through apply_workflow when MCP returns planPath or next.tool=apply_workflow.`,
131
+ },
132
+ {
133
+ code: "workflow-validate-apply-report",
134
+ kind: "validation",
135
+ confidence: "high",
136
+ action:
137
+ "After apply_workflow, call run_validation with validationTarget when present; otherwise use applyReportPath.",
138
+ message: "Validate the apply report artifact so diagnostics and recommendations follow the actual apply result.",
139
+ },
140
+ ...(spec.name === "add-field" ? createAddFieldRecommendations(inputs) : []),
141
+ ];
142
+
143
+ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string, unknown>): WorkflowPlan => {
144
+ const inputs: Record<string, WorkflowInputValue> = {};
145
+ const diagnostics: WorkflowDiagnostic[] = [];
146
+
147
+ for (const [name, inputSpec] of Object.entries(spec.inputs)) {
148
+ const rawValue = rawInputs[name];
149
+ if (rawValue === undefined || rawValue === null || rawValue === "") {
150
+ if (inputSpec.required) {
151
+ diagnostics.push({
152
+ severity: "error",
153
+ code: "workflow-input-missing",
154
+ input: name,
155
+ message: `Workflow ${spec.name} requires input "${name}".`,
156
+ });
157
+ }
158
+ continue;
159
+ }
160
+
161
+ const value = normalizeInputValue(name, inputSpec, rawValue);
162
+ if (value === null) {
163
+ diagnostics.push({
164
+ severity: "error",
165
+ code: "workflow-input-invalid",
166
+ input: name,
167
+ message: `Workflow ${spec.name} input "${name}" must be ${inputSpec.type}.`,
168
+ });
169
+ continue;
170
+ }
171
+ if (inputSpec.allowedValues && typeof value === "string" && !inputSpec.allowedValues.includes(value)) {
172
+ diagnostics.push({
173
+ severity: "error",
174
+ code: "workflow-input-not-allowed",
175
+ input: name,
176
+ message: `Workflow ${spec.name} input "${name}" must be one of: ${inputSpec.allowedValues.join(", ")}.`,
177
+ });
178
+ continue;
179
+ }
180
+ inputs[name] = value;
181
+ }
182
+
183
+ const fieldType = inputs.type;
184
+ if (
185
+ spec.name === "add-field" &&
186
+ typeof fieldType === "string" &&
187
+ (fieldType.toLowerCase() === "number" || fieldType.toLowerCase() === "numeric")
188
+ ) {
189
+ diagnostics.push({
190
+ severity: "error",
191
+ code: "primitive-field-type-unsupported",
192
+ input: "type",
193
+ message: `Field type "${fieldType}" is ambiguous in Akan. Use Int for integer fields or Float for decimal fields.`,
194
+ });
195
+ }
196
+
197
+ return {
198
+ schemaVersion: 1,
199
+ workflow: spec.name,
200
+ mode: "plan",
201
+ inputs,
202
+ optionalSurfaces: spec.optionalSurfaces ?? {},
203
+ steps: spec.steps,
204
+ predictedChanges: spec.predictedChanges,
205
+ validation: spec.validation,
206
+ diagnostics,
207
+ recommendations: createWorkflowPlanRecommendations(spec, inputs),
208
+ requiresApproval: true,
209
+ };
210
+ };
@@ -0,0 +1,59 @@
1
+ import type { PrimitiveFormat, PrimitiveWriteReport } from "./types";
2
+ import { jsonText } from "./utils";
3
+
4
+ export const createPrimitiveWriteReport = ({
5
+ command,
6
+ status,
7
+ changedFiles = [],
8
+ generatedFiles = [],
9
+ validationCommands = [],
10
+ diagnostics = [],
11
+ nextActions = [],
12
+ }: Omit<PrimitiveWriteReport, "schemaVersion" | "status"> & {
13
+ status?: PrimitiveWriteReport["status"];
14
+ }): PrimitiveWriteReport => ({
15
+ schemaVersion: 1,
16
+ command,
17
+ status: status ?? (diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed"),
18
+ changedFiles,
19
+ generatedFiles,
20
+ validationCommands,
21
+ diagnostics,
22
+ nextActions,
23
+ });
24
+
25
+ export const renderPrimitiveWriteReport = (report: PrimitiveWriteReport) =>
26
+ [
27
+ `# Primitive Write: ${report.command}`,
28
+ "",
29
+ `- Status: ${report.status}`,
30
+ "",
31
+ "## Changed Files",
32
+ ...(report.changedFiles.length
33
+ ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
34
+ : ["- none"]),
35
+ "",
36
+ "## Generated Files",
37
+ ...(report.generatedFiles.length
38
+ ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
39
+ : ["- none"]),
40
+ "",
41
+ "## Validation Commands",
42
+ ...(report.validationCommands.length
43
+ ? report.validationCommands.map((validation) => `- \`${validation.command}\`: ${validation.reason}`)
44
+ : ["- none"]),
45
+ "",
46
+ "## Diagnostics",
47
+ ...(report.diagnostics.length
48
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
49
+ : ["- none"]),
50
+ "",
51
+ "## Next Actions",
52
+ ...(report.nextActions.length
53
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
54
+ : ["- none"]),
55
+ "",
56
+ ].join("\n");
57
+
58
+ export const renderPrimitiveReport = (report: PrimitiveWriteReport, format: PrimitiveFormat = "markdown") =>
59
+ format === "json" ? jsonText(report) : renderPrimitiveWriteReport(report);
@@ -0,0 +1,215 @@
1
+ import type {
2
+ RepairReport,
3
+ WorkflowApplyReport,
4
+ WorkflowFormat,
5
+ WorkflowPlan,
6
+ WorkflowRunArtifact,
7
+ WorkflowSpec,
8
+ WorkflowValidationRunReport,
9
+ } from "./types";
10
+ import { jsonText } from "./utils";
11
+
12
+ export const renderWorkflowList = (specs: readonly WorkflowSpec[]) =>
13
+ [
14
+ "# Akan Workflows",
15
+ "",
16
+ ...specs.flatMap((spec) => [`- \`${spec.name}\`: ${spec.description}`, ` - When: ${spec.whenToUse}`]),
17
+ "",
18
+ ].join("\n");
19
+
20
+ export const renderWorkflowExplain = (spec: WorkflowSpec) =>
21
+ [
22
+ `# Workflow: ${spec.name}`,
23
+ "",
24
+ spec.description,
25
+ "",
26
+ "## When To Use",
27
+ spec.whenToUse,
28
+ "",
29
+ "## Inputs",
30
+ ...Object.entries(spec.inputs).map(
31
+ ([name, input]) =>
32
+ `- \`${name}\`${input.required ? " (required)" : ""}: ${input.description}${
33
+ input.allowedValues ? ` Allowed: ${input.allowedValues.join(", ")}.` : ""
34
+ }`,
35
+ ),
36
+ "",
37
+ "## Optional Surfaces",
38
+ ...Object.entries(spec.optionalSurfaces ?? {}).map(([name, mode]) => `- \`${name}\`: ${mode}`),
39
+ "",
40
+ "## Steps",
41
+ ...spec.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
42
+ "",
43
+ "## Validation",
44
+ ...spec.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
45
+ "",
46
+ ].join("\n");
47
+
48
+ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
49
+ [
50
+ `# Workflow Plan: ${plan.workflow}`,
51
+ "",
52
+ `- Mode: ${plan.mode}`,
53
+ `- Requires approval: ${plan.requiresApproval}`,
54
+ "",
55
+ "## Inputs",
56
+ ...Object.entries(plan.inputs).map(
57
+ ([name, value]) => `- \`${name}\`: ${Array.isArray(value) ? value.join(", ") : value}`,
58
+ ),
59
+ "",
60
+ "## Optional Surfaces",
61
+ ...Object.entries(plan.optionalSurfaces).map(([name, mode]) => `- \`${name}\`: ${mode}`),
62
+ "",
63
+ "## Steps",
64
+ ...plan.steps.map((step, index) => `${index + 1}. \`${step.id}\` (${step.tool}): ${step.description}`),
65
+ "",
66
+ "## Predicted Changes",
67
+ ...plan.predictedChanges.map((change) => {
68
+ const scope = change.applyScope ? ` (${change.applyScope})` : "";
69
+ return `- \`${change.action}\`${scope} ${change.target}: ${change.reason}`;
70
+ }),
71
+ "",
72
+ "## Validation",
73
+ ...plan.validation.map((validation) => `- \`${validation.command}\`: ${validation.reason}`),
74
+ "",
75
+ "## Diagnostics",
76
+ ...(plan.diagnostics.length
77
+ ? plan.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
78
+ : ["- none"]),
79
+ "",
80
+ "## Recommendations",
81
+ ...(plan.recommendations.length
82
+ ? plan.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
83
+ : ["- none"]),
84
+ "",
85
+ ].join("\n");
86
+
87
+ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) =>
88
+ [
89
+ `# Workflow Apply: ${report.workflow}`,
90
+ "",
91
+ `- Mode: ${report.mode}`,
92
+ `- Status: ${report.status}`,
93
+ "",
94
+ "## Changed Files",
95
+ ...(report.changedFiles.length
96
+ ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
97
+ : ["- none"]),
98
+ "",
99
+ "## Generated Files",
100
+ ...(report.generatedFiles.length
101
+ ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
102
+ : ["- none"]),
103
+ "",
104
+ "## Applied Commands",
105
+ ...(report.appliedCommands.length
106
+ ? report.appliedCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
107
+ : ["- none"]),
108
+ "",
109
+ "## Recommended Validation Commands",
110
+ ...(report.recommendedValidationCommands.length
111
+ ? report.recommendedValidationCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
112
+ : ["- none"]),
113
+ "",
114
+ "## Diagnostics",
115
+ ...(report.diagnostics.length
116
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
117
+ : ["- none"]),
118
+ "",
119
+ "## Recommendations",
120
+ ...(report.recommendations.length
121
+ ? report.recommendations.map((recommendation) => `- [${recommendation.kind}] ${recommendation.message}`)
122
+ : ["- none"]),
123
+ "",
124
+ "## Next Actions",
125
+ ...(report.nextActions.length
126
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
127
+ : ["- none"]),
128
+ "",
129
+ ].join("\n");
130
+
131
+ export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
132
+ format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
133
+
134
+ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
135
+ [
136
+ `# Workflow Validation: ${report.workflow}`,
137
+ "",
138
+ `- Run: ${report.runId}`,
139
+ `- Status: ${report.status}`,
140
+ `- Source status: ${report.sourceStatus}`,
141
+ `- Workspace status: ${report.workspaceStatus}`,
142
+ `- Overall status: ${report.overallStatus}`,
143
+ "",
144
+ "## Known Blockers",
145
+ ...(report.knownBlockers.length
146
+ ? report.knownBlockers.map((blocker) => {
147
+ const command = blocker.command ? ` \`${blocker.command}\`` : "";
148
+ const count = blocker.count > 1 ? ` (${blocker.count}x)` : "";
149
+ return `- [${blocker.failureScope}]${command}${count}: ${blocker.message}`;
150
+ })
151
+ : ["- none"]),
152
+ "",
153
+ "## Commands",
154
+ ...(report.commands.length
155
+ ? report.commands.map((command) => {
156
+ const scope = command.failureScope ? ` (${command.failureScope})` : "";
157
+ return `- [${command.status}] \`${command.command}\`${scope}: ${command.reason}`;
158
+ })
159
+ : ["- none"]),
160
+ "",
161
+ "## Diagnostics",
162
+ ...(report.diagnostics.length
163
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
164
+ : ["- none"]),
165
+ "",
166
+ "## Repair Actions",
167
+ ...(report.repairActions.length
168
+ ? report.repairActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
169
+ : ["- none"]),
170
+ "",
171
+ "## Next Actions",
172
+ ...(report.nextActions.length
173
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
174
+ : ["- none"]),
175
+ "",
176
+ ].join("\n");
177
+
178
+ export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
179
+ format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
180
+
181
+ export const renderRepairReportMarkdown = (report: RepairReport) =>
182
+ [
183
+ `# Akan Repair: ${report.kind}`,
184
+ "",
185
+ `- Status: ${report.status}`,
186
+ `- Target: ${report.target ?? "none"}`,
187
+ "",
188
+ "## Commands",
189
+ ...(report.commands.length
190
+ ? report.commands.map((command) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
191
+ : ["- none"]),
192
+ "",
193
+ "## Diagnostics",
194
+ ...(report.diagnostics.length
195
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
196
+ : ["- none"]),
197
+ "",
198
+ "## Next Actions",
199
+ ...(report.nextActions.length
200
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
201
+ : ["- none"]),
202
+ "",
203
+ ].join("\n");
204
+
205
+ export const renderRepairReport = (report: RepairReport, format: WorkflowFormat = "markdown") =>
206
+ format === "json" ? jsonText(report) : renderRepairReportMarkdown(report);
207
+
208
+ export const renderWorkflowRunArtifact = (artifact: WorkflowRunArtifact, format: WorkflowFormat = "markdown") => {
209
+ if ("kind" in artifact) return renderRepairReport(artifact, format);
210
+ if ("mode" in artifact && artifact.mode === "validate") return renderWorkflowValidation(artifact, format);
211
+ if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
212
+ return renderWorkflowApply(artifact, format);
213
+ }
214
+ return jsonText(artifact);
215
+ };