@akanjs/devkit 2.3.9-rc.1 → 2.3.9-rc.10
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 +195 -14
- package/akanMcpContract.test.ts +37 -0
- package/akanMcpContract.ts +701 -0
- package/devkitUtils.test.ts +2 -2
- package/executors.ts +22 -17
- package/getModelFileData.ts +5 -2
- package/index.ts +1 -0
- package/package.json +2 -2
- package/workflow/artifacts.ts +571 -0
- package/workflow/executor.ts +590 -0
- package/workflow/index.ts +11 -1089
- package/workflow/moduleIndex.test.ts +296 -0
- package/workflow/moduleIndex.ts +504 -0
- package/workflow/plan.ts +429 -0
- package/workflow/primitive.ts +59 -0
- package/workflow/render.ts +335 -0
- package/workflow/rolloutGate.test.ts +109 -0
- package/workflow/rolloutGate.ts +141 -0
- package/workflow/source.test.ts +62 -0
- package/workflow/source.ts +864 -0
- package/workflow/types.ts +475 -0
- package/workflow/uiPolicy.ts +45 -0
- package/workflow/utils.ts +23 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import type { Workspace } from "../commandDecorators";
|
|
2
|
+
import type {
|
|
3
|
+
GeneratedSyncState,
|
|
4
|
+
PrimitiveChangedFile,
|
|
5
|
+
PrimitiveGeneratedFile,
|
|
6
|
+
RepairAction,
|
|
7
|
+
RepairReport,
|
|
8
|
+
WorkflowApplyCommand,
|
|
9
|
+
WorkflowApplyReport,
|
|
10
|
+
WorkflowBaselineSummary,
|
|
11
|
+
WorkflowDiagnostic,
|
|
12
|
+
WorkflowFailureScope,
|
|
13
|
+
WorkflowKnownBlocker,
|
|
14
|
+
WorkflowNextActionCode,
|
|
15
|
+
WorkflowOverallStatus,
|
|
16
|
+
WorkflowPlan,
|
|
17
|
+
WorkflowRunArtifact,
|
|
18
|
+
WorkflowRunSource,
|
|
19
|
+
WorkflowValidationCommandResult,
|
|
20
|
+
WorkflowValidationRunReport,
|
|
21
|
+
WorkflowValidationStatus,
|
|
22
|
+
} from "./types";
|
|
23
|
+
import { commandStatus, jsonText, uniqueBy, workflowStatus } from "./utils";
|
|
24
|
+
|
|
25
|
+
const sourceChangeBlocked = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
26
|
+
diagnostics.some(
|
|
27
|
+
(diagnostic) =>
|
|
28
|
+
diagnostic.severity === "error" &&
|
|
29
|
+
(diagnostic.failureScope === "source-change" ||
|
|
30
|
+
!diagnostic.failureScope ||
|
|
31
|
+
diagnostic.failureScope === "unknown"),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const workflowPlanApproval = {
|
|
35
|
+
required: true,
|
|
36
|
+
meaning: "Review this read-only plan before apply_workflow mutates files.",
|
|
37
|
+
applyTool: "apply_workflow",
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
const inferNextActionCode = (
|
|
41
|
+
action: { action?: WorkflowNextActionCode; command: string },
|
|
42
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
43
|
+
): WorkflowNextActionCode => {
|
|
44
|
+
if (action.action) return action.action;
|
|
45
|
+
if (sourceChangeBlocked(diagnostics) && action.command.startsWith("akan workflow explain")) return "blocked";
|
|
46
|
+
if (action.command.startsWith("akan workflow repair")) return "repair";
|
|
47
|
+
if (action.command.startsWith("akan workflow explain")) return "manual-review";
|
|
48
|
+
if (action.command.startsWith("akan ")) return "validate";
|
|
49
|
+
return "answer";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const nextActionPriority = (
|
|
53
|
+
action: { action?: WorkflowNextActionCode },
|
|
54
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
55
|
+
) => {
|
|
56
|
+
const priority = sourceChangeBlocked(diagnostics)
|
|
57
|
+
? { blocked: 0, repair: 1, "manual-review": 2, validate: 3, answer: 4 }
|
|
58
|
+
: { "manual-review": 0, validate: 1, repair: 2, blocked: 3, answer: 4 };
|
|
59
|
+
return priority[action.action ?? "answer"];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const createWorkflowApplyReport = ({
|
|
63
|
+
workflow,
|
|
64
|
+
mode,
|
|
65
|
+
changedFiles = [],
|
|
66
|
+
generatedFiles = [],
|
|
67
|
+
appliedCommands = [],
|
|
68
|
+
recommendedValidationCommands,
|
|
69
|
+
commands = [],
|
|
70
|
+
diagnostics = [],
|
|
71
|
+
postApplyChecks = [],
|
|
72
|
+
recommendations = [],
|
|
73
|
+
nextActions = [],
|
|
74
|
+
plan,
|
|
75
|
+
}: Omit<
|
|
76
|
+
WorkflowApplyReport,
|
|
77
|
+
| "schemaVersion"
|
|
78
|
+
| "runId"
|
|
79
|
+
| "applyReportPath"
|
|
80
|
+
| "validationTarget"
|
|
81
|
+
| "status"
|
|
82
|
+
| "summary"
|
|
83
|
+
| "appliedCommands"
|
|
84
|
+
| "recommendedValidationCommands"
|
|
85
|
+
| "commands"
|
|
86
|
+
| "postApplyChecks"
|
|
87
|
+
| "recommendations"
|
|
88
|
+
> & {
|
|
89
|
+
appliedCommands?: WorkflowApplyCommand[];
|
|
90
|
+
recommendedValidationCommands?: WorkflowApplyCommand[];
|
|
91
|
+
commands?: WorkflowApplyCommand[];
|
|
92
|
+
postApplyChecks?: WorkflowApplyReport["postApplyChecks"];
|
|
93
|
+
recommendations?: WorkflowApplyReport["recommendations"];
|
|
94
|
+
}): WorkflowApplyReport => {
|
|
95
|
+
const validationCommands = recommendedValidationCommands ?? commands;
|
|
96
|
+
const nextActionsWithIntent = nextActions.map((action) => ({
|
|
97
|
+
...action,
|
|
98
|
+
action: inferNextActionCode(action, diagnostics),
|
|
99
|
+
}));
|
|
100
|
+
if (sourceChangeBlocked(diagnostics) && !nextActionsWithIntent.some((action) => action.action === "blocked")) {
|
|
101
|
+
nextActionsWithIntent.unshift({
|
|
102
|
+
command: `akan workflow explain ${workflow}`,
|
|
103
|
+
reason: "Review source-change blockers before running validation.",
|
|
104
|
+
action: "blocked",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const orderedNextActions = uniqueBy(nextActionsWithIntent, (action) => action.command).sort(
|
|
108
|
+
(left, right) => nextActionPriority(left, diagnostics) - nextActionPriority(right, diagnostics),
|
|
109
|
+
);
|
|
110
|
+
const sourceFilesChanged = uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
|
|
111
|
+
const generatedFilesSynced = uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
|
|
112
|
+
return {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
workflow,
|
|
115
|
+
mode,
|
|
116
|
+
status: workflowStatus(diagnostics),
|
|
117
|
+
summary: {
|
|
118
|
+
sourceFilesChanged,
|
|
119
|
+
generatedFilesSynced,
|
|
120
|
+
},
|
|
121
|
+
changedFiles: sourceFilesChanged,
|
|
122
|
+
generatedFiles: generatedFilesSynced,
|
|
123
|
+
appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
|
|
124
|
+
recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
|
|
125
|
+
commands: uniqueBy(validationCommands, (command) => command.command),
|
|
126
|
+
diagnostics,
|
|
127
|
+
postApplyChecks,
|
|
128
|
+
recommendations: uniqueBy(recommendations, (recommendation) => `${recommendation.kind}:${recommendation.code}`),
|
|
129
|
+
nextActions: orderedNextActions.slice(0, 3),
|
|
130
|
+
plan,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
|
|
135
|
+
const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
|
|
136
|
+
return command
|
|
137
|
+
.replaceAll("<app-or-lib-or-pkg>", target)
|
|
138
|
+
.replaceAll("<app-or-lib>", target)
|
|
139
|
+
.replaceAll("<app-name>", target);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
|
|
143
|
+
plan.validation.map((validation) => ({
|
|
144
|
+
command: resolveWorkflowCommand(validation.command, plan),
|
|
145
|
+
reason: validation.reason,
|
|
146
|
+
kind: validation.kind,
|
|
147
|
+
})) satisfies WorkflowApplyCommand[];
|
|
148
|
+
|
|
149
|
+
export const workflowRunsDir = ".akan/workflows/runs";
|
|
150
|
+
|
|
151
|
+
export const createWorkflowRunId = (prefix = "run") =>
|
|
152
|
+
`${prefix}-${new Date()
|
|
153
|
+
.toISOString()
|
|
154
|
+
.replace(/[-:.TZ]/g, "")
|
|
155
|
+
.slice(0, 14)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
156
|
+
|
|
157
|
+
const getRunId = (artifact: WorkflowRunArtifact) => {
|
|
158
|
+
if ("runId" in artifact && artifact.runId) return artifact.runId;
|
|
159
|
+
return createWorkflowRunId("mode" in artifact ? artifact.mode : artifact.kind);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const workflowRunArtifactPath = (runId: string) => `${workflowRunsDir}/${runId}.json`;
|
|
163
|
+
|
|
164
|
+
const withWorkflowRunMetadata = (
|
|
165
|
+
artifact: WorkflowRunArtifact,
|
|
166
|
+
runId: string,
|
|
167
|
+
artifactPath: string,
|
|
168
|
+
): WorkflowRunArtifact => {
|
|
169
|
+
if ("mode" in artifact && (artifact.mode === "apply" || artifact.mode === "dry-run")) {
|
|
170
|
+
return { ...artifact, runId, applyReportPath: artifactPath, validationTarget: artifactPath };
|
|
171
|
+
}
|
|
172
|
+
if ("kind" in artifact) return { ...artifact, runId, repairReportPath: artifactPath };
|
|
173
|
+
return artifact;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const writeWorkflowRunArtifact = async (workspace: Workspace, artifact: WorkflowRunArtifact) => {
|
|
177
|
+
const runId = getRunId(artifact);
|
|
178
|
+
const artifactPath = workflowRunArtifactPath(runId);
|
|
179
|
+
const artifactWithMetadata = withWorkflowRunMetadata(artifact, runId, artifactPath);
|
|
180
|
+
await workspace.writeFile(artifactPath, jsonText(artifactWithMetadata), { silent: true });
|
|
181
|
+
return { runId, path: artifactPath, artifact: artifactWithMetadata };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const workflowSyncDir = ".akan/workflows/sync";
|
|
185
|
+
|
|
186
|
+
const syncStateSlug = (target: string) =>
|
|
187
|
+
target
|
|
188
|
+
.trim()
|
|
189
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
190
|
+
.replace(/^-+|-+$/g, "")
|
|
191
|
+
.toLowerCase();
|
|
192
|
+
|
|
193
|
+
export const workflowSyncStatePath = (target: string) => `${workflowSyncDir}/${syncStateSlug(target) || "target"}.json`;
|
|
194
|
+
|
|
195
|
+
export const generatedFilePathsForTarget = (targetRoot: string, reason = "Generated files were refreshed by sync.") =>
|
|
196
|
+
[
|
|
197
|
+
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
198
|
+
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
199
|
+
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
200
|
+
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason },
|
|
201
|
+
] satisfies PrimitiveGeneratedFile[];
|
|
202
|
+
|
|
203
|
+
export const writeGeneratedSyncState = async (workspace: Workspace, state: GeneratedSyncState) => {
|
|
204
|
+
const statePath = workflowSyncStatePath(state.target);
|
|
205
|
+
await workspace.writeFile(statePath, jsonText(state), { silent: true });
|
|
206
|
+
return statePath;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const readWorkflowRunArtifact = async (workspace: Workspace, runId: string) => {
|
|
210
|
+
const artifactPath = workflowRunArtifactPath(runId);
|
|
211
|
+
if (!(await workspace.exists(artifactPath))) throw new Error(`Workflow run artifact does not exist: ${artifactPath}`);
|
|
212
|
+
return (await workspace.readJson(artifactPath)) as WorkflowRunArtifact;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export type WorkflowValidationCommandExecutor = (
|
|
216
|
+
command: WorkflowApplyCommand,
|
|
217
|
+
) => Promise<WorkflowValidationCommandResult>;
|
|
218
|
+
|
|
219
|
+
const failedCommandScopes = (commands: readonly WorkflowValidationCommandResult[]) =>
|
|
220
|
+
commands.filter((command) => command.status === "failed").map((command) => command.failureScope ?? "unknown");
|
|
221
|
+
|
|
222
|
+
const errorDiagnosticScopes = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
223
|
+
diagnostics
|
|
224
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
225
|
+
.map(
|
|
226
|
+
(diagnostic) =>
|
|
227
|
+
diagnostic.failureScope ??
|
|
228
|
+
(diagnostic.scope === "baseline"
|
|
229
|
+
? "workspace-config"
|
|
230
|
+
: diagnostic.scope === "workflow"
|
|
231
|
+
? "source-change"
|
|
232
|
+
: "unknown"),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const hasScopeFailure = (
|
|
236
|
+
scopes: readonly WorkflowFailureScope[],
|
|
237
|
+
scope: WorkflowFailureScope,
|
|
238
|
+
diagnostics: readonly WorkflowDiagnostic[] = [],
|
|
239
|
+
) =>
|
|
240
|
+
scopes.includes(scope) ||
|
|
241
|
+
diagnostics.some((diagnostic) => diagnostic.severity === "error" && diagnostic.failureScope === scope);
|
|
242
|
+
|
|
243
|
+
const statusForScope = (
|
|
244
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
245
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
246
|
+
scopes: readonly WorkflowFailureScope[],
|
|
247
|
+
expectedScope: WorkflowFailureScope,
|
|
248
|
+
): WorkflowValidationStatus => {
|
|
249
|
+
const hasCommands = commands.length > 0 || diagnostics.length > 0;
|
|
250
|
+
if (!hasCommands) return "unknown";
|
|
251
|
+
return hasScopeFailure(scopes, expectedScope, diagnostics) ? "failed" : "passed";
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const statusForValidationKind = (
|
|
255
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
256
|
+
kind: WorkflowValidationCommandResult["kind"],
|
|
257
|
+
): WorkflowValidationStatus => {
|
|
258
|
+
const matching = commands.filter((command) => command.kind === kind);
|
|
259
|
+
if (matching.length === 0) return "unknown";
|
|
260
|
+
return matching.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const statusForCommands = (commands: readonly WorkflowValidationCommandResult[]): WorkflowValidationStatus => {
|
|
264
|
+
if (commands.length === 0) return "unknown";
|
|
265
|
+
return commandStatus(commands) === "failed" ? "failed" : "passed";
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const statusForDiagnostics = (diagnostics: readonly WorkflowDiagnostic[]): WorkflowValidationStatus => {
|
|
269
|
+
if (diagnostics.length === 0) return "unknown";
|
|
270
|
+
return workflowStatus(diagnostics) === "failed" ? "failed" : "passed";
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const workflowDiagnosticContextPaths = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
274
|
+
uniqueBy(
|
|
275
|
+
diagnostics.flatMap((diagnostic) => diagnostic.context?.paths ?? []),
|
|
276
|
+
(filePath) => filePath,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
export const createWorkflowBaselineSummary = (
|
|
280
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
281
|
+
{ detailsIncluded = true, knownBlockerCount = 0 }: { detailsIncluded?: boolean; knownBlockerCount?: number } = {},
|
|
282
|
+
): WorkflowBaselineSummary => {
|
|
283
|
+
const grouped = new Map<string, WorkflowBaselineSummary["byCode"][number]>();
|
|
284
|
+
let totalErrors = 0;
|
|
285
|
+
let totalWarnings = 0;
|
|
286
|
+
for (const diagnostic of diagnostics) {
|
|
287
|
+
if (diagnostic.severity === "error") totalErrors += 1;
|
|
288
|
+
else totalWarnings += 1;
|
|
289
|
+
const existing = grouped.get(diagnostic.code);
|
|
290
|
+
if (existing) {
|
|
291
|
+
existing.count += 1;
|
|
292
|
+
if (existing.severity !== diagnostic.severity) existing.severity = "mixed";
|
|
293
|
+
} else {
|
|
294
|
+
grouped.set(diagnostic.code, {
|
|
295
|
+
code: diagnostic.code,
|
|
296
|
+
severity: diagnostic.severity,
|
|
297
|
+
count: 1,
|
|
298
|
+
sampleMessage: diagnostic.message,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const contextPaths = workflowDiagnosticContextPaths(diagnostics);
|
|
303
|
+
return {
|
|
304
|
+
status: totalErrors > 0 ? "failed" : diagnostics.length > 0 ? "passed" : "unknown",
|
|
305
|
+
total: diagnostics.length,
|
|
306
|
+
totalErrors,
|
|
307
|
+
totalWarnings,
|
|
308
|
+
detailsIncluded,
|
|
309
|
+
knownBlockerCount,
|
|
310
|
+
byCode: [...grouped.values()],
|
|
311
|
+
...(contextPaths.length ? { contextPaths } : {}),
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const createKnownBlockers = (
|
|
316
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
317
|
+
diagnostics: readonly WorkflowDiagnostic[],
|
|
318
|
+
): WorkflowKnownBlocker[] => {
|
|
319
|
+
const commandBlockers = commands
|
|
320
|
+
.filter(
|
|
321
|
+
(command) =>
|
|
322
|
+
command.status === "failed" &&
|
|
323
|
+
(command.failureScope === "workspace-config" || command.failureScope === "environment"),
|
|
324
|
+
)
|
|
325
|
+
.map((command) => ({
|
|
326
|
+
code: `workflow-validation-${command.failureScope}`,
|
|
327
|
+
message: `${command.failureScope === "environment" ? "Environment" : "Workspace configuration"} blocker: ${command.command}`,
|
|
328
|
+
failureScope: command.failureScope ?? "unknown",
|
|
329
|
+
command: command.command,
|
|
330
|
+
kind: command.kind,
|
|
331
|
+
count: 1,
|
|
332
|
+
}));
|
|
333
|
+
const diagnosticBlockers = diagnostics
|
|
334
|
+
.filter(
|
|
335
|
+
(diagnostic) =>
|
|
336
|
+
diagnostic.severity === "error" &&
|
|
337
|
+
(diagnostic.failureScope === "workspace-config" ||
|
|
338
|
+
diagnostic.failureScope === "environment" ||
|
|
339
|
+
diagnostic.scope === "baseline"),
|
|
340
|
+
)
|
|
341
|
+
.map((diagnostic) => ({
|
|
342
|
+
code: diagnostic.code,
|
|
343
|
+
message: diagnostic.message,
|
|
344
|
+
failureScope: diagnostic.failureScope ?? ("workspace-config" as const),
|
|
345
|
+
command: diagnostic.command,
|
|
346
|
+
kind: diagnostic.kind,
|
|
347
|
+
count: 1,
|
|
348
|
+
}));
|
|
349
|
+
const grouped = new Map<string, WorkflowKnownBlocker>();
|
|
350
|
+
for (const blocker of [...commandBlockers, ...diagnosticBlockers]) {
|
|
351
|
+
const key = `${blocker.failureScope}:${blocker.code}:${blocker.command ?? ""}:${blocker.message}`;
|
|
352
|
+
const existing = grouped.get(key);
|
|
353
|
+
if (existing) existing.count += blocker.count;
|
|
354
|
+
else grouped.set(key, blocker);
|
|
355
|
+
}
|
|
356
|
+
return [...grouped.values()];
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const createValidationStatuses = (
|
|
360
|
+
commands: readonly WorkflowValidationCommandResult[],
|
|
361
|
+
reportDiagnostics: readonly WorkflowDiagnostic[],
|
|
362
|
+
baselineDiagnostics: readonly WorkflowDiagnostic[],
|
|
363
|
+
workflowDiagnostics: readonly WorkflowDiagnostic[],
|
|
364
|
+
): {
|
|
365
|
+
sourceStatus: WorkflowValidationStatus;
|
|
366
|
+
workspaceStatus: WorkflowValidationStatus;
|
|
367
|
+
validationCommandsStatus: WorkflowValidationStatus;
|
|
368
|
+
baselineStatus: WorkflowValidationStatus;
|
|
369
|
+
overallStatus: WorkflowOverallStatus;
|
|
370
|
+
summary: {
|
|
371
|
+
sourceChange: WorkflowValidationStatus;
|
|
372
|
+
generatedSync: WorkflowValidationStatus;
|
|
373
|
+
validationCommands: WorkflowValidationStatus;
|
|
374
|
+
baseline: WorkflowValidationStatus;
|
|
375
|
+
workspaceConfig: WorkflowValidationStatus;
|
|
376
|
+
environment: WorkflowValidationStatus;
|
|
377
|
+
};
|
|
378
|
+
} => {
|
|
379
|
+
const diagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
380
|
+
const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
|
|
381
|
+
const sourceDiagnostics = [...reportDiagnostics, ...workflowDiagnostics].filter(
|
|
382
|
+
(diagnostic) =>
|
|
383
|
+
diagnostic.failureScope === "source-change" ||
|
|
384
|
+
diagnostic.scope === "workflow" ||
|
|
385
|
+
(!diagnostic.failureScope && diagnostic.scope !== "baseline"),
|
|
386
|
+
);
|
|
387
|
+
const sourceScopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(sourceDiagnostics)];
|
|
388
|
+
const sourceStatus = statusForScope(commands, sourceDiagnostics, sourceScopes, "source-change");
|
|
389
|
+
const validationCommandsStatus = statusForCommands(commands);
|
|
390
|
+
const baselineStatus = statusForDiagnostics(baselineDiagnostics);
|
|
391
|
+
const nonBaselineDiagnostics = [...reportDiagnostics, ...workflowDiagnostics];
|
|
392
|
+
const workspaceStatus =
|
|
393
|
+
hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
|
|
394
|
+
? "failed"
|
|
395
|
+
: commands.length || diagnostics.length
|
|
396
|
+
? "passed"
|
|
397
|
+
: "unknown";
|
|
398
|
+
const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
|
|
399
|
+
? "failed"
|
|
400
|
+
: validationCommandsStatus === "passed" &&
|
|
401
|
+
baselineStatus === "failed" &&
|
|
402
|
+
workflowStatus(nonBaselineDiagnostics) !== "failed"
|
|
403
|
+
? "passed-with-baseline-blockers"
|
|
404
|
+
: hasScopeFailure(scopes, "workspace-config", diagnostics)
|
|
405
|
+
? "blocked-by-workspace-config"
|
|
406
|
+
: hasScopeFailure(scopes, "environment", diagnostics)
|
|
407
|
+
? "blocked-by-environment"
|
|
408
|
+
: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed"
|
|
409
|
+
? "failed"
|
|
410
|
+
: "passed";
|
|
411
|
+
return {
|
|
412
|
+
sourceStatus,
|
|
413
|
+
workspaceStatus,
|
|
414
|
+
validationCommandsStatus,
|
|
415
|
+
baselineStatus,
|
|
416
|
+
overallStatus,
|
|
417
|
+
summary: {
|
|
418
|
+
sourceChange: sourceStatus,
|
|
419
|
+
generatedSync: statusForValidationKind(commands, "sync"),
|
|
420
|
+
validationCommands: validationCommandsStatus,
|
|
421
|
+
baseline: baselineStatus,
|
|
422
|
+
workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
|
|
423
|
+
environment: statusForScope(commands, diagnostics, scopes, "environment"),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export const createWorkflowValidationRunReport = async ({
|
|
429
|
+
runId = createWorkflowRunId("validation"),
|
|
430
|
+
workflow,
|
|
431
|
+
source,
|
|
432
|
+
plan,
|
|
433
|
+
commands,
|
|
434
|
+
execute,
|
|
435
|
+
diagnostics = [],
|
|
436
|
+
baselineDiagnostics = [],
|
|
437
|
+
workflowDiagnostics = [],
|
|
438
|
+
repairActions = [],
|
|
439
|
+
}: {
|
|
440
|
+
runId?: string;
|
|
441
|
+
workflow: string;
|
|
442
|
+
source: WorkflowRunSource;
|
|
443
|
+
plan?: WorkflowPlan;
|
|
444
|
+
commands: WorkflowApplyCommand[];
|
|
445
|
+
execute: WorkflowValidationCommandExecutor;
|
|
446
|
+
diagnostics?: WorkflowDiagnostic[];
|
|
447
|
+
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
448
|
+
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
449
|
+
repairActions?: RepairAction[];
|
|
450
|
+
}): Promise<WorkflowValidationRunReport> => {
|
|
451
|
+
const results: WorkflowValidationCommandResult[] = [];
|
|
452
|
+
for (const command of commands) {
|
|
453
|
+
results.push(await execute(command));
|
|
454
|
+
}
|
|
455
|
+
const commandDiagnostics = results.flatMap((result) =>
|
|
456
|
+
result.status === "failed"
|
|
457
|
+
? [
|
|
458
|
+
{
|
|
459
|
+
severity: "error" as const,
|
|
460
|
+
code: "workflow-validation-command-failed",
|
|
461
|
+
message: `Validation command failed: ${result.command}`,
|
|
462
|
+
command: result.command,
|
|
463
|
+
kind: result.kind,
|
|
464
|
+
failureScope: result.failureScope,
|
|
465
|
+
},
|
|
466
|
+
]
|
|
467
|
+
: [],
|
|
468
|
+
);
|
|
469
|
+
const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
|
|
470
|
+
const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
471
|
+
const knownBlockers = createKnownBlockers(results, scopedDiagnostics);
|
|
472
|
+
const statuses = createValidationStatuses(results, reportDiagnostics, baselineDiagnostics, workflowDiagnostics);
|
|
473
|
+
return {
|
|
474
|
+
schemaVersion: 1,
|
|
475
|
+
runId,
|
|
476
|
+
workflow,
|
|
477
|
+
mode: "validate",
|
|
478
|
+
source,
|
|
479
|
+
status: statuses.overallStatus === "passed" ? "passed" : "failed",
|
|
480
|
+
...statuses,
|
|
481
|
+
knownBlockers,
|
|
482
|
+
commands: results,
|
|
483
|
+
diagnostics: reportDiagnostics,
|
|
484
|
+
baselineSummary: createWorkflowBaselineSummary(baselineDiagnostics, {
|
|
485
|
+
knownBlockerCount: knownBlockers.filter((blocker) => blocker.failureScope === "workspace-config").length,
|
|
486
|
+
}),
|
|
487
|
+
baselineDiagnostics,
|
|
488
|
+
workflowDiagnostics,
|
|
489
|
+
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
490
|
+
nextActions: results
|
|
491
|
+
.filter((result) => result.status === "failed")
|
|
492
|
+
.map((result) => ({ command: result.command, reason: "Re-run this validation command after repair." })),
|
|
493
|
+
plan,
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
498
|
+
const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
499
|
+
if (change.action !== "create" && change.action !== "modify") return [];
|
|
500
|
+
return [
|
|
501
|
+
{
|
|
502
|
+
path: change.target,
|
|
503
|
+
action: change.action,
|
|
504
|
+
reason: change.reason,
|
|
505
|
+
},
|
|
506
|
+
];
|
|
507
|
+
});
|
|
508
|
+
const generatedFiles: PrimitiveGeneratedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
509
|
+
if (change.action !== "sync") return [];
|
|
510
|
+
return [
|
|
511
|
+
{
|
|
512
|
+
path: change.target,
|
|
513
|
+
action: "sync",
|
|
514
|
+
reason: change.reason,
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
});
|
|
518
|
+
const diagnostics = [...plan.diagnostics];
|
|
519
|
+
return createWorkflowApplyReport({
|
|
520
|
+
workflow: plan.workflow,
|
|
521
|
+
mode: "dry-run",
|
|
522
|
+
changedFiles,
|
|
523
|
+
generatedFiles,
|
|
524
|
+
commands: workflowCommandsForPlan(plan),
|
|
525
|
+
diagnostics,
|
|
526
|
+
recommendations: plan.recommendations,
|
|
527
|
+
nextActions: workflowCommandsForPlan(plan),
|
|
528
|
+
plan,
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
export const createRepairReport = ({
|
|
533
|
+
command,
|
|
534
|
+
kind,
|
|
535
|
+
target = null,
|
|
536
|
+
diagnostics = [],
|
|
537
|
+
repairActions = [],
|
|
538
|
+
nextActions = [],
|
|
539
|
+
commands = [],
|
|
540
|
+
generatedFiles = [],
|
|
541
|
+
syncedAt,
|
|
542
|
+
}: Omit<
|
|
543
|
+
RepairReport,
|
|
544
|
+
| "schemaVersion"
|
|
545
|
+
| "runId"
|
|
546
|
+
| "repairReportPath"
|
|
547
|
+
| "status"
|
|
548
|
+
| "target"
|
|
549
|
+
| "diagnostics"
|
|
550
|
+
| "repairActions"
|
|
551
|
+
| "nextActions"
|
|
552
|
+
| "commands"
|
|
553
|
+
> &
|
|
554
|
+
Partial<
|
|
555
|
+
Pick<
|
|
556
|
+
RepairReport,
|
|
557
|
+
"target" | "diagnostics" | "repairActions" | "nextActions" | "commands" | "generatedFiles" | "syncedAt"
|
|
558
|
+
>
|
|
559
|
+
>): RepairReport => ({
|
|
560
|
+
schemaVersion: 1,
|
|
561
|
+
command,
|
|
562
|
+
kind,
|
|
563
|
+
target,
|
|
564
|
+
status: workflowStatus(diagnostics) === "failed" || commandStatus(commands) === "failed" ? "failed" : "passed",
|
|
565
|
+
diagnostics,
|
|
566
|
+
repairActions: uniqueBy(repairActions, (action) => action.command),
|
|
567
|
+
nextActions: uniqueBy(nextActions, (action) => action.command),
|
|
568
|
+
commands,
|
|
569
|
+
generatedFiles,
|
|
570
|
+
...(syncedAt ? { syncedAt } : {}),
|
|
571
|
+
});
|