@akanjs/devkit 2.3.9-rc.3 → 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.
@@ -0,0 +1,375 @@
1
+ import { capitalize } from "akanjs/common";
2
+ import { coerceFieldDefault, moduleSourcePaths } 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
+ import { addFieldUiPolicyForType } from "./uiPolicy";
14
+
15
+ const surfaceModes = new Set<WorkflowSurfaceMode>(["infer", "include", "skip"]);
16
+
17
+ const parseStringList = (value: unknown) => {
18
+ if (Array.isArray(value)) {
19
+ const values = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
20
+ return values.length === value.length ? values : null;
21
+ }
22
+ if (typeof value !== "string") return null;
23
+ return value
24
+ .split(",")
25
+ .map((item) => item.trim())
26
+ .filter(Boolean);
27
+ };
28
+
29
+ const normalizeInputValue = (name: string, spec: WorkflowInputSpec, value: unknown): WorkflowInputValue | null => {
30
+ if (spec.type === "string") return typeof value === "string" && value.length > 0 ? value : null;
31
+ if (spec.type === "string-list") {
32
+ const values = parseStringList(value);
33
+ return values && values.length > 0 ? values : null;
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
+ }
43
+ if (typeof value === "string" && surfaceModes.has(value as WorkflowSurfaceMode)) return value as WorkflowSurfaceMode;
44
+ throw new Error(`Unsupported workflow input value for ${name}`);
45
+ };
46
+
47
+ export const listWorkflowSpecs = (specs: readonly WorkflowSpec[]) =>
48
+ [...specs].sort((a, b) => a.name.localeCompare(b.name));
49
+
50
+ export const getWorkflowSpec = (specs: readonly WorkflowSpec[], name: string) =>
51
+ specs.find((spec) => spec.name === name) ?? null;
52
+
53
+ export const compactWorkflowInputs = (inputs: WorkflowPlanInputs) =>
54
+ Object.fromEntries(Object.entries(inputs).filter(([, value]) => value !== null && value !== ""));
55
+
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
+ ];
97
+ };
98
+
99
+ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
100
+ const app = typeof inputs.app === "string" ? inputs.app : "<app-or-lib>";
101
+ const module = typeof inputs.module === "string" ? inputs.module : "<module>";
102
+ const field = typeof inputs.field === "string" ? inputs.field : "<field>";
103
+ const typeName = typeof inputs.type === "string" ? inputs.type : null;
104
+ if (!typeName) return [];
105
+
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;
112
+ return [
113
+ ...(normalizedType === "Int" || normalizedType === "Float"
114
+ ? [
115
+ {
116
+ code: "add-field-import",
117
+ kind: "import" as const,
118
+ target: paths.constant,
119
+ confidence: "high" as const,
120
+ message: `Import ${normalizedType} from "akanjs/base" before writing field(${normalizedType}).`,
121
+ },
122
+ ]
123
+ : []),
124
+ {
125
+ code: "add-field-placement-constant",
126
+ kind: "placement",
127
+ target: paths.constant,
128
+ confidence: "high",
129
+ message: `Insert ${field}: field(${normalizedType}) in ${capitalize(module)}Input.`,
130
+ },
131
+ {
132
+ code: "add-field-placement-dictionary",
133
+ kind: "placement",
134
+ target: paths.dictionary,
135
+ confidence: "high",
136
+ message: `Add dictionary labels for ${module}.${field}.`,
137
+ },
138
+ {
139
+ code: "add-field-component",
140
+ kind: "ui-component",
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}.`,
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
+ : []),
194
+ {
195
+ code: "add-field-ui-manual-review",
196
+ kind: "manual-action",
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.`,
201
+ confidence: "medium",
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.",
252
+ },
253
+ ];
254
+ };
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
+
268
+ const createWorkflowPlanRecommendations = (
269
+ spec: WorkflowSpec,
270
+ inputs: Record<string, WorkflowInputValue>,
271
+ ): WorkflowRecommendation[] => [
272
+ {
273
+ code: "workflow-apply-first",
274
+ kind: "auto-apply",
275
+ confidence: "high",
276
+ action: "Call apply_workflow with the MCP planPath before editing source files directly.",
277
+ message: `Apply the ${spec.name} plan through apply_workflow when MCP returns planPath or next.tool=apply_workflow.`,
278
+ },
279
+ {
280
+ code: "workflow-validate-apply-report",
281
+ kind: "validation",
282
+ confidence: "high",
283
+ action:
284
+ "After apply_workflow, call run_validation with validationTarget when present; otherwise use applyReportPath.",
285
+ message: "Validate the apply report artifact so diagnostics and recommendations follow the actual apply result.",
286
+ },
287
+ ...(spec.name === "add-field" ? createAddFieldRecommendations(inputs) : []),
288
+ ];
289
+
290
+ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string, unknown>): WorkflowPlan => {
291
+ const inputs: Record<string, WorkflowInputValue> = {};
292
+ const diagnostics: WorkflowDiagnostic[] = [];
293
+
294
+ for (const [name, inputSpec] of Object.entries(spec.inputs)) {
295
+ const rawValue = rawInputs[name];
296
+ if (rawValue === undefined || rawValue === null || rawValue === "") {
297
+ if (inputSpec.required) {
298
+ diagnostics.push({
299
+ severity: "error",
300
+ code: "workflow-input-missing",
301
+ input: name,
302
+ message: `Workflow ${spec.name} requires input "${name}".`,
303
+ });
304
+ }
305
+ continue;
306
+ }
307
+
308
+ const value = normalizeInputValue(name, inputSpec, rawValue);
309
+ if (value === null) {
310
+ diagnostics.push({
311
+ severity: "error",
312
+ code: "workflow-input-invalid",
313
+ input: name,
314
+ message: `Workflow ${spec.name} input "${name}" must be ${inputSpec.type}.`,
315
+ });
316
+ continue;
317
+ }
318
+ if (inputSpec.allowedValues && typeof value === "string" && !inputSpec.allowedValues.includes(value)) {
319
+ diagnostics.push({
320
+ severity: "error",
321
+ code: "workflow-input-not-allowed",
322
+ input: name,
323
+ message: `Workflow ${spec.name} input "${name}" must be one of: ${inputSpec.allowedValues.join(", ")}.`,
324
+ });
325
+ continue;
326
+ }
327
+ inputs[name] = value;
328
+ }
329
+
330
+ const fieldType = inputs.type;
331
+ if (
332
+ spec.name === "add-field" &&
333
+ typeof fieldType === "string" &&
334
+ (fieldType.toLowerCase() === "number" || fieldType.toLowerCase() === "numeric")
335
+ ) {
336
+ diagnostics.push({
337
+ severity: "error",
338
+ code: "primitive-field-type-unsupported",
339
+ input: "type",
340
+ message: `Field type "${fieldType}" is ambiguous in Akan. Use Int for integer fields or Float for decimal fields.`,
341
+ });
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
+ }
361
+
362
+ return {
363
+ schemaVersion: 1,
364
+ workflow: spec.name,
365
+ mode: "plan",
366
+ inputs,
367
+ optionalSurfaces: createWorkflowPlanOptionalSurfaces(spec, inputs),
368
+ steps: spec.steps,
369
+ predictedChanges: createWorkflowPlanPredictedChanges(spec, inputs),
370
+ validation: spec.validation,
371
+ diagnostics,
372
+ recommendations: createWorkflowPlanRecommendations(spec, inputs),
373
+ requiresApproval: true,
374
+ };
375
+ };
@@ -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,252 @@
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
+ 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 [
111
+ `# Workflow Apply: ${report.workflow}`,
112
+ "",
113
+ `- Mode: ${report.mode}`,
114
+ `- Status: ${report.status}`,
115
+ ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
116
+ "",
117
+ "## Automatically Modified",
118
+ ...(report.changedFiles.length
119
+ ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
120
+ : ["- none"]),
121
+ "",
122
+ "## Generated Sync",
123
+ ...(report.generatedFiles.length
124
+ ? report.generatedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
125
+ : ["- none"]),
126
+ "",
127
+ "## Applied Commands",
128
+ ...(report.appliedCommands.length
129
+ ? report.appliedCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
130
+ : ["- none"]),
131
+ "",
132
+ "## Recommended Validation Commands",
133
+ ...(report.recommendedValidationCommands.length
134
+ ? report.recommendedValidationCommands.map((command) => `- \`${command.command}\`: ${command.reason}`)
135
+ : ["- none"]),
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
+ "",
147
+ "## Diagnostics",
148
+ ...(report.diagnostics.length ? report.diagnostics.map(renderDiagnostic) : ["- none"]),
149
+ "",
150
+ "## Recommendations",
151
+ ...(report.recommendations.length ? report.recommendations.map(renderRecommendation) : ["- none"]),
152
+ "",
153
+ "## Next Actions",
154
+ ...(report.nextActions.length
155
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
156
+ : ["- none"]),
157
+ "",
158
+ ].join("\n");
159
+ };
160
+
161
+ export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
162
+ format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
163
+
164
+ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
165
+ [
166
+ `# Workflow Validation: ${report.workflow}`,
167
+ "",
168
+ `- Run: ${report.runId}`,
169
+ `- Status: ${report.status}`,
170
+ `- Source status: ${report.sourceStatus}`,
171
+ `- Workspace status: ${report.workspaceStatus}`,
172
+ `- Overall status: ${report.overallStatus}`,
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
+ "",
180
+ "## Known Blockers",
181
+ ...(report.knownBlockers.length
182
+ ? report.knownBlockers.map((blocker) => {
183
+ const command = blocker.command ? ` \`${blocker.command}\`` : "";
184
+ const count = blocker.count > 1 ? ` (${blocker.count}x)` : "";
185
+ const known = blocker.known ? " known" : "";
186
+ return `- [${blocker.failureScope}${known}]${command}${count}: ${blocker.message}`;
187
+ })
188
+ : ["- none"]),
189
+ "",
190
+ "## Commands",
191
+ ...(report.commands.length
192
+ ? report.commands.map((command) => {
193
+ const scope = command.failureScope ? ` (${command.failureScope})` : "";
194
+ return `- [${command.status}] \`${command.command}\`${scope}: ${command.reason}`;
195
+ })
196
+ : ["- none"]),
197
+ "",
198
+ "## Diagnostics",
199
+ ...(report.diagnostics.length
200
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
201
+ : ["- none"]),
202
+ "",
203
+ "## Repair Actions",
204
+ ...(report.repairActions.length
205
+ ? report.repairActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
206
+ : ["- none"]),
207
+ "",
208
+ "## Next Actions",
209
+ ...(report.nextActions.length
210
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
211
+ : ["- none"]),
212
+ "",
213
+ ].join("\n");
214
+
215
+ export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
216
+ format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
217
+
218
+ export const renderRepairReportMarkdown = (report: RepairReport) =>
219
+ [
220
+ `# Akan Repair: ${report.kind}`,
221
+ "",
222
+ `- Status: ${report.status}`,
223
+ `- Target: ${report.target ?? "none"}`,
224
+ "",
225
+ "## Commands",
226
+ ...(report.commands.length
227
+ ? report.commands.map((command) => `- [${command.status}] \`${command.command}\`: ${command.reason}`)
228
+ : ["- none"]),
229
+ "",
230
+ "## Diagnostics",
231
+ ...(report.diagnostics.length
232
+ ? report.diagnostics.map((diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`)
233
+ : ["- none"]),
234
+ "",
235
+ "## Next Actions",
236
+ ...(report.nextActions.length
237
+ ? report.nextActions.map((action) => `- \`${action.command}\`: ${action.reason}`)
238
+ : ["- none"]),
239
+ "",
240
+ ].join("\n");
241
+
242
+ export const renderRepairReport = (report: RepairReport, format: WorkflowFormat = "markdown") =>
243
+ format === "json" ? jsonText(report) : renderRepairReportMarkdown(report);
244
+
245
+ export const renderWorkflowRunArtifact = (artifact: WorkflowRunArtifact, format: WorkflowFormat = "markdown") => {
246
+ if ("kind" in artifact) return renderRepairReport(artifact, format);
247
+ if ("mode" in artifact && artifact.mode === "validate") return renderWorkflowValidation(artifact, format);
248
+ if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
249
+ return renderWorkflowApply(artifact, format);
250
+ }
251
+ return jsonText(artifact);
252
+ };