@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.
package/akanContext.ts CHANGED
@@ -713,8 +713,14 @@ export class AkanContextAnalyzer {
713
713
  };
714
714
  }
715
715
 
716
- static findModules(context: AkanWorkspaceContext, moduleName?: string | null) {
717
- const modules = [...context.apps, ...context.libs].flatMap((sys) => sys.modules);
716
+ static findModules(
717
+ context: AkanWorkspaceContext,
718
+ moduleName?: string | null,
719
+ { app = null }: { app?: string | null } = {},
720
+ ) {
721
+ const modules = [...context.apps, ...context.libs]
722
+ .filter((sys) => !app || sys.name === app)
723
+ .flatMap((sys) => sys.modules);
718
724
  return moduleName
719
725
  ? modules.filter((module) => module.name === moduleName || module.folderName === moduleName)
720
726
  : modules;
@@ -269,11 +269,11 @@ describe("getModelFileData", () => {
269
269
  ].join("\n"),
270
270
  );
271
271
  await write(
272
- path.join(root, "apps/demo/lib/post/post.Unit.tsx"),
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/post.View.tsx"),
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]: any } = {},
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]: any },
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]: any };
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]: any };
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: any[]; // ESLint.LintResult[];
830
+ results: unknown[]; // ESLint.LintResult[];
831
831
  message: string;
832
- errors: any[]; // ESLintLinter.LintMessage[];
833
- warnings: any[]; // ESLintLinter.LintMessage[];
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) => this.getLocalFile(`lib/${viewComponent}/${viewComponent}.View.tsx`)),
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) => this.getLocalFile(`lib/${unitComponent}/${unitComponent}.Unit.tsx`)),
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
  }
@@ -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}/${modelName}.Unit.tsx`;
21
- const viewFilePath = `${modulePath}/lib/${modelName}/${modelName}.View.tsx`;
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",
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.3",
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",
@@ -0,0 +1,415 @@
1
+ import type { Workspace } from "../commandDecorators";
2
+ import type {
3
+ GeneratedSyncState,
4
+ PrimitiveChangedFile,
5
+ PrimitiveGeneratedFile,
6
+ RepairAction,
7
+ RepairReport,
8
+ WorkflowApplyCommand,
9
+ WorkflowApplyReport,
10
+ WorkflowDiagnostic,
11
+ WorkflowFailureScope,
12
+ WorkflowKnownBlocker,
13
+ WorkflowPlan,
14
+ WorkflowRunArtifact,
15
+ WorkflowRunSource,
16
+ WorkflowValidationCommandResult,
17
+ WorkflowValidationRunReport,
18
+ WorkflowValidationStatus,
19
+ } from "./types";
20
+ import { commandStatus, jsonText, uniqueBy, workflowStatus } from "./utils";
21
+
22
+ export const createWorkflowApplyReport = ({
23
+ workflow,
24
+ mode,
25
+ changedFiles = [],
26
+ generatedFiles = [],
27
+ appliedCommands = [],
28
+ recommendedValidationCommands,
29
+ commands = [],
30
+ diagnostics = [],
31
+ recommendations = [],
32
+ nextActions = [],
33
+ plan,
34
+ }: Omit<
35
+ WorkflowApplyReport,
36
+ | "schemaVersion"
37
+ | "runId"
38
+ | "applyReportPath"
39
+ | "validationTarget"
40
+ | "status"
41
+ | "appliedCommands"
42
+ | "recommendedValidationCommands"
43
+ | "commands"
44
+ | "recommendations"
45
+ > & {
46
+ appliedCommands?: WorkflowApplyCommand[];
47
+ recommendedValidationCommands?: WorkflowApplyCommand[];
48
+ commands?: WorkflowApplyCommand[];
49
+ recommendations?: WorkflowApplyReport["recommendations"];
50
+ }): WorkflowApplyReport => {
51
+ const validationCommands = recommendedValidationCommands ?? commands;
52
+ return {
53
+ schemaVersion: 1,
54
+ workflow,
55
+ mode,
56
+ status: workflowStatus(diagnostics),
57
+ changedFiles: uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
58
+ generatedFiles: uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`),
59
+ appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
60
+ recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
61
+ commands: uniqueBy(validationCommands, (command) => command.command),
62
+ diagnostics,
63
+ recommendations: uniqueBy(recommendations, (recommendation) => `${recommendation.kind}:${recommendation.code}`),
64
+ nextActions: uniqueBy(nextActions, (action) => action.command),
65
+ plan,
66
+ };
67
+ };
68
+
69
+ export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
70
+ const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
71
+ return command
72
+ .replaceAll("<app-or-lib-or-pkg>", target)
73
+ .replaceAll("<app-or-lib>", target)
74
+ .replaceAll("<app-name>", target);
75
+ };
76
+
77
+ export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
78
+ plan.validation.map((validation) => ({
79
+ command: resolveWorkflowCommand(validation.command, plan),
80
+ reason: validation.reason,
81
+ kind: validation.kind,
82
+ })) satisfies WorkflowApplyCommand[];
83
+
84
+ export const workflowRunsDir = ".akan/workflows/runs";
85
+
86
+ export const createWorkflowRunId = (prefix = "run") =>
87
+ `${prefix}-${new Date()
88
+ .toISOString()
89
+ .replace(/[-:.TZ]/g, "")
90
+ .slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
91
+
92
+ const getRunId = (artifact: WorkflowRunArtifact) => {
93
+ if ("runId" in artifact && artifact.runId) return artifact.runId;
94
+ return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
95
+ };
96
+
97
+ export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
98
+
99
+ const withWorkflowRunMetadata = (
100
+ artifact: WorkflowRunArtifact,
101
+ runId: string,
102
+ artifactPath: string,
103
+ ): WorkflowRunArtifact => {
104
+ if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
105
+ return { ...artifact, runId, applyReportPath: artifactPath, validationTarget: artifactPath };
106
+ }
107
+ if ("kind" in artifact) return { ...artifact, runId, repairReportPath: artifactPath };
108
+ return artifact;
109
+ };
110
+
111
+ export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
112
+ const runId = getRunId(artifact);
113
+ const artifactPath = workflowRunArtifactPath(runId);
114
+ const artifactWithMetadata = withWorkflowRunMetadata(artifact, runId, artifactPath);
115
+ await workspace.writeFile(artifactPath, jsonText(artifactWithMetadata), { silent: true });
116
+ return { runId, path: artifactPath, artifact: artifactWithMetadata };
117
+ };
118
+
119
+ export const workflowSyncDir = ".akan/workflows/sync";
120
+
121
+ const syncStateSlug = (target: string) =>
122
+ target
123
+ .trim()
124
+ .replace(/[^a-zA-Z0-9]+/g, "-")
125
+ .replace(/^-+|-+$/g, "")
126
+ .toLowerCase();
127
+
128
+ export const workflowSyncStatePath = (target: string) => `${workflowSyncDir}/${syncStateSlug(target) || "target"}.json`;
129
+
130
+ export const generatedFilePathsForTarget = (targetRoot: string, reason = "Generated files were refreshed by sync.") =>
131
+ [
132
+ { path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
133
+ { path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
134
+ { path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
135
+ { path: `${targetRoot}/lib/index.ts`, action: "sync", reason },
136
+ ] satisfies PrimitiveGeneratedFile[];
137
+
138
+ export const writeGeneratedSyncState = async (workspace: Workspace, state: GeneratedSyncState) => {
139
+ const statePath = workflowSyncStatePath(state.target);
140
+ await workspace.writeFile(statePath, jsonText(state), { silent: true });
141
+ return statePath;
142
+ };
143
+
144
+ export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
145
+ const artifactPath = workflowRunArtifactPath(runId);
146
+ if (!(await workspace.exists(artifactPath))) throw new Error(`Workflow run artifact does not exist: ${artifactPath}`);
147
+ return (await workspace.readJson(artifactPath)) as WorkflowRunArtifact;
148
+ };
149
+
150
+ export type WorkflowValidationCommandExecutor = (
151
+ command: WorkflowApplyCommand,
152
+ ) => Promise<WorkflowValidationCommandResult>;
153
+
154
+ const failedCommandScopes = (commands: readonly WorkflowValidationCommandResult[]) =>
155
+ commands.filter((command) => command.status === "failed").map((command) => command.failureScope ?? "unknown");
156
+
157
+ const errorDiagnosticScopes = (diagnostics: readonly WorkflowDiagnostic[]) =>
158
+ diagnostics
159
+ .filter((diagnostic) => diagnostic.severity === "error")
160
+ .map(
161
+ (diagnostic) =>
162
+ diagnostic.failureScope ??
163
+ (diagnostic.scope === "baseline"
164
+ ? "workspace-config"
165
+ : diagnostic.scope === "workflow"
166
+ ? "source-change"
167
+ : "unknown"),
168
+ );
169
+
170
+ const hasScopeFailure = (
171
+ scopes: readonly WorkflowFailureScope[],
172
+ scope: WorkflowFailureScope,
173
+ diagnostics: readonly WorkflowDiagnostic[] = [],
174
+ ) =>
175
+ scopes.includes(scope) ||
176
+ diagnostics.some((diagnostic) => diagnostic.severity === "error" && diagnostic.failureScope === scope);
177
+
178
+ const statusForScope = (
179
+ commands: readonly WorkflowValidationCommandResult[],
180
+ diagnostics: readonly WorkflowDiagnostic[],
181
+ scopes: readonly WorkflowFailureScope[],
182
+ expectedScope: WorkflowFailureScope,
183
+ ): WorkflowValidationStatus => {
184
+ const hasCommands = commands.length > 0 || diagnostics.length > 0;
185
+ if (!hasCommands) return "unknown";
186
+ return hasScopeFailure(scopes, expectedScope, diagnostics) ? "failed" : "passed";
187
+ };
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
+
198
+ const createKnownBlockers = (
199
+ commands: readonly WorkflowValidationCommandResult[],
200
+ diagnostics: readonly WorkflowDiagnostic[],
201
+ ): WorkflowKnownBlocker[] => {
202
+ const commandBlockers = commands
203
+ .filter(
204
+ (command) =>
205
+ command.status === "failed" &&
206
+ (command.failureScope === "workspace-config" || command.failureScope === "environment"),
207
+ )
208
+ .map((command) => ({
209
+ code: `workflow-validation-${command.failureScope}`,
210
+ message: `${command.failureScope === "environment" ? "Environment" : "Workspace configuration"} blocker: ${command.command}`,
211
+ failureScope: command.failureScope ?? "unknown",
212
+ command: command.command,
213
+ kind: command.kind,
214
+ count: 1,
215
+ }));
216
+ const diagnosticBlockers = diagnostics
217
+ .filter(
218
+ (diagnostic) =>
219
+ diagnostic.severity === "error" &&
220
+ (diagnostic.failureScope === "workspace-config" ||
221
+ diagnostic.failureScope === "environment" ||
222
+ diagnostic.scope === "baseline"),
223
+ )
224
+ .map((diagnostic) => ({
225
+ code: diagnostic.code,
226
+ message: diagnostic.message,
227
+ failureScope: diagnostic.failureScope ?? ("workspace-config" as const),
228
+ command: diagnostic.command,
229
+ kind: diagnostic.kind,
230
+ count: 1,
231
+ }));
232
+ const grouped = new Map<string, WorkflowKnownBlocker>();
233
+ for (const blocker of [...commandBlockers, ...diagnosticBlockers]) {
234
+ const key = `${blocker.failureScope}:${blocker.code}:${blocker.command ?? ""}:${blocker.message}`;
235
+ const existing = grouped.get(key);
236
+ if (existing) existing.count += blocker.count;
237
+ else grouped.set(key, blocker);
238
+ }
239
+ return [...grouped.values()];
240
+ };
241
+
242
+ const createValidationStatuses = (
243
+ commands: readonly WorkflowValidationCommandResult[],
244
+ diagnostics: readonly WorkflowDiagnostic[],
245
+ ) => {
246
+ const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
247
+ const sourceStatus = statusForScope(commands, diagnostics, scopes, "source-change");
248
+ const workspaceStatus =
249
+ hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
250
+ ? "failed"
251
+ : commands.length || diagnostics.length
252
+ ? "passed"
253
+ : "unknown";
254
+ const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
255
+ ? "failed"
256
+ : hasScopeFailure(scopes, "workspace-config", diagnostics)
257
+ ? "blocked-by-workspace-config"
258
+ : hasScopeFailure(scopes, "environment", diagnostics)
259
+ ? "blocked-by-environment"
260
+ : workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
261
+ ? "failed"
262
+ : "passed";
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
+ };
274
+ };
275
+
276
+ export const createWorkflowValidationRunReport = async ({
277
+ runId = createWorkflowRunId("validation"),
278
+ workflow,
279
+ source,
280
+ plan,
281
+ commands,
282
+ execute,
283
+ diagnostics = [],
284
+ baselineDiagnostics = [],
285
+ workflowDiagnostics = [],
286
+ repairActions = [],
287
+ }: {
288
+ runId?: string;
289
+ workflow: string;
290
+ source: WorkflowRunSource;
291
+ plan?: WorkflowPlan;
292
+ commands: WorkflowApplyCommand[];
293
+ execute: WorkflowValidationCommandExecutor;
294
+ diagnostics?: WorkflowDiagnostic[];
295
+ baselineDiagnostics?: WorkflowDiagnostic[];
296
+ workflowDiagnostics?: WorkflowDiagnostic[];
297
+ repairActions?: RepairAction[];
298
+ }): Promise<WorkflowValidationRunReport> => {
299
+ const results: WorkflowValidationCommandResult[] = [];
300
+ for (const command of commands) {
301
+ results.push(await execute(command));
302
+ }
303
+ const commandDiagnostics = results.flatMap((result) =>
304
+ result.status === "failed"
305
+ ? [
306
+ {
307
+ severity: "error" as const,
308
+ code: "workflow-validation-command-failed",
309
+ message: `Validation command failed: ${result.command}`,
310
+ command: result.command,
311
+ kind: result.kind,
312
+ failureScope: result.failureScope,
313
+ },
314
+ ]
315
+ : [],
316
+ );
317
+ const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
318
+ const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
319
+ const statuses = createValidationStatuses(results, scopedDiagnostics);
320
+ return {
321
+ schemaVersion: 1,
322
+ runId,
323
+ workflow,
324
+ mode: "validate",
325
+ source,
326
+ status: statuses.overallStatus === "passed" ? "passed" : "failed",
327
+ ...statuses,
328
+ knownBlockers: createKnownBlockers(results, scopedDiagnostics),
329
+ commands: results,
330
+ diagnostics: reportDiagnostics,
331
+ baselineDiagnostics,
332
+ workflowDiagnostics,
333
+ repairActions: uniqueBy(repairActions, (action) => action.command),
334
+ nextActions: results
335
+ .filter((result) => result.status === "failed")
336
+ .map((result) => ({ command: result.command, reason: "Re-run this validation command after repair." })),
337
+ plan,
338
+ };
339
+ };
340
+
341
+ export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
342
+ const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
343
+ if (change.action !== "create" && change.action !== "modify") return [];
344
+ return [
345
+ {
346
+ path: change.target,
347
+ action: change.action,
348
+ reason: change.reason,
349
+ },
350
+ ];
351
+ });
352
+ const generatedFiles: PrimitiveGeneratedFile[] = plan.predictedChanges.flatMap((change) => {
353
+ if (change.action !== "sync") return [];
354
+ return [
355
+ {
356
+ path: change.target,
357
+ action: "sync",
358
+ reason: change.reason,
359
+ },
360
+ ];
361
+ });
362
+ const diagnostics = [...plan.diagnostics];
363
+ return createWorkflowApplyReport({
364
+ workflow: plan.workflow,
365
+ mode: "dry-run",
366
+ changedFiles,
367
+ generatedFiles,
368
+ commands: workflowCommandsForPlan(plan),
369
+ diagnostics,
370
+ recommendations: plan.recommendations,
371
+ nextActions: workflowCommandsForPlan(plan),
372
+ plan,
373
+ });
374
+ };
375
+
376
+ export const createRepairReport = ({
377
+ command,
378
+ kind,
379
+ target = null,
380
+ diagnostics = [],
381
+ repairActions = [],
382
+ nextActions = [],
383
+ commands = [],
384
+ generatedFiles = [],
385
+ syncedAt,
386
+ }: Omit<
387
+ RepairReport,
388
+ | "schemaVersion"
389
+ | "runId"
390
+ | "repairReportPath"
391
+ | "status"
392
+ | "target"
393
+ | "diagnostics"
394
+ | "repairActions"
395
+ | "nextActions"
396
+ | "commands"
397
+ > &
398
+ Partial<
399
+ Pick<
400
+ RepairReport,
401
+ "target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
402
+ >
403
+ >): RepairReport => ({
404
+ schemaVersion: 1,
405
+ command,
406
+ kind,
407
+ target,
408
+ status: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed" ? "failed" : "passed",
409
+ diagnostics,
410
+ repairActions: uniqueBy(repairActions, (action) => action.command),
411
+ nextActions: uniqueBy(nextActions, (action) => action.command),
412
+ commands,
413
+ generatedFiles,
414
+ ...(syncedAt ? { syncedAt } : {}),
415
+ });