@akanjs/devkit 2.3.9-rc.7 → 2.3.9-rc.9
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/akanMcpContract.test.ts +37 -0
- package/akanMcpContract.ts +701 -0
- package/index.ts +1 -0
- package/package.json +2 -2
- package/workflow/artifacts.ts +166 -19
- package/workflow/executor.ts +211 -7
- package/workflow/index.ts +2 -0
- package/workflow/moduleIndex.test.ts +296 -0
- package/workflow/moduleIndex.ts +504 -0
- package/workflow/plan.ts +61 -7
- package/workflow/render.ts +49 -10
- package/workflow/rolloutGate.test.ts +109 -0
- package/workflow/rolloutGate.ts +141 -0
- package/workflow/source.test.ts +62 -0
- package/workflow/source.ts +463 -75
- package/workflow/types.ts +172 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.3.9-rc.
|
|
3
|
+
"version": "2.3.9-rc.9",
|
|
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.
|
|
35
|
+
"akanjs": "2.3.9-rc.9",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "5.5.23",
|
package/workflow/artifacts.ts
CHANGED
|
@@ -7,9 +7,12 @@ import type {
|
|
|
7
7
|
RepairReport,
|
|
8
8
|
WorkflowApplyCommand,
|
|
9
9
|
WorkflowApplyReport,
|
|
10
|
+
WorkflowBaselineSummary,
|
|
10
11
|
WorkflowDiagnostic,
|
|
11
12
|
WorkflowFailureScope,
|
|
12
13
|
WorkflowKnownBlocker,
|
|
14
|
+
WorkflowNextActionCode,
|
|
15
|
+
WorkflowOverallStatus,
|
|
13
16
|
WorkflowPlan,
|
|
14
17
|
WorkflowRunArtifact,
|
|
15
18
|
WorkflowRunSource,
|
|
@@ -19,6 +22,43 @@ import type {
|
|
|
19
22
|
} from "./types";
|
|
20
23
|
import { commandStatus, jsonText, uniqueBy, workflowStatus } from "./utils";
|
|
21
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
|
+
|
|
22
62
|
export const createWorkflowApplyReport = ({
|
|
23
63
|
workflow,
|
|
24
64
|
mode,
|
|
@@ -39,6 +79,7 @@ export const createWorkflowApplyReport = ({
|
|
|
39
79
|
| "applyReportPath"
|
|
40
80
|
| "validationTarget"
|
|
41
81
|
| "status"
|
|
82
|
+
| "summary"
|
|
42
83
|
| "appliedCommands"
|
|
43
84
|
| "recommendedValidationCommands"
|
|
44
85
|
| "commands"
|
|
@@ -52,18 +93,33 @@ export const createWorkflowApplyReport = ({
|
|
|
52
93
|
recommendations?: WorkflowApplyReport["recommendations"];
|
|
53
94
|
}): WorkflowApplyReport => {
|
|
54
95
|
const validationCommands = recommendedValidationCommands ?? commands;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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}`);
|
|
60
112
|
return {
|
|
61
113
|
schemaVersion: 1,
|
|
62
114
|
workflow,
|
|
63
115
|
mode,
|
|
64
116
|
status: workflowStatus(diagnostics),
|
|
65
|
-
|
|
66
|
-
|
|
117
|
+
summary: {
|
|
118
|
+
sourceFilesChanged,
|
|
119
|
+
generatedFilesSynced,
|
|
120
|
+
},
|
|
121
|
+
changedFiles: sourceFilesChanged,
|
|
122
|
+
generatedFiles: generatedFilesSynced,
|
|
67
123
|
appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
|
|
68
124
|
recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
|
|
69
125
|
commands: uniqueBy(validationCommands, (command) => command.command),
|
|
@@ -204,6 +260,58 @@ const statusForValidationKind = (
|
|
|
204
260
|
return matching.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
205
261
|
};
|
|
206
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
|
+
|
|
207
315
|
const createKnownBlockers = (
|
|
208
316
|
commands: readonly WorkflowValidationCommandResult[],
|
|
209
317
|
diagnostics: readonly WorkflowDiagnostic[],
|
|
@@ -250,10 +358,37 @@ const createKnownBlockers = (
|
|
|
250
358
|
|
|
251
359
|
const createValidationStatuses = (
|
|
252
360
|
commands: readonly WorkflowValidationCommandResult[],
|
|
253
|
-
|
|
254
|
-
|
|
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];
|
|
255
380
|
const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
|
|
256
|
-
const
|
|
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];
|
|
257
392
|
const workspaceStatus =
|
|
258
393
|
hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
|
|
259
394
|
? "failed"
|
|
@@ -262,20 +397,28 @@ const createValidationStatuses = (
|
|
|
262
397
|
: "unknown";
|
|
263
398
|
const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
|
|
264
399
|
? "failed"
|
|
265
|
-
:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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";
|
|
272
411
|
return {
|
|
273
412
|
sourceStatus,
|
|
274
413
|
workspaceStatus,
|
|
414
|
+
validationCommandsStatus,
|
|
415
|
+
baselineStatus,
|
|
275
416
|
overallStatus,
|
|
276
417
|
summary: {
|
|
277
418
|
sourceChange: sourceStatus,
|
|
278
419
|
generatedSync: statusForValidationKind(commands, "sync"),
|
|
420
|
+
validationCommands: validationCommandsStatus,
|
|
421
|
+
baseline: baselineStatus,
|
|
279
422
|
workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
|
|
280
423
|
environment: statusForScope(commands, diagnostics, scopes, "environment"),
|
|
281
424
|
},
|
|
@@ -325,7 +468,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
325
468
|
);
|
|
326
469
|
const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
|
|
327
470
|
const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
328
|
-
const
|
|
471
|
+
const knownBlockers = createKnownBlockers(results, scopedDiagnostics);
|
|
472
|
+
const statuses = createValidationStatuses(results, reportDiagnostics, baselineDiagnostics, workflowDiagnostics);
|
|
329
473
|
return {
|
|
330
474
|
schemaVersion: 1,
|
|
331
475
|
runId,
|
|
@@ -334,9 +478,12 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
334
478
|
source,
|
|
335
479
|
status: statuses.overallStatus === "passed" ? "passed" : "failed",
|
|
336
480
|
...statuses,
|
|
337
|
-
knownBlockers
|
|
481
|
+
knownBlockers,
|
|
338
482
|
commands: results,
|
|
339
483
|
diagnostics: reportDiagnostics,
|
|
484
|
+
baselineSummary: createWorkflowBaselineSummary(baselineDiagnostics, {
|
|
485
|
+
knownBlockerCount: knownBlockers.filter((blocker) => blocker.failureScope === "workspace-config").length,
|
|
486
|
+
}),
|
|
340
487
|
baselineDiagnostics,
|
|
341
488
|
workflowDiagnostics,
|
|
342
489
|
repairActions: uniqueBy(repairActions, (action) => action.command),
|
package/workflow/executor.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { capitalize } from "akanjs/common";
|
|
2
1
|
import ts from "typescript";
|
|
2
|
+
import type { AkanModuleContext } from "../akanContext";
|
|
3
3
|
import type { Sys, Workspace } from "../commandDecorators";
|
|
4
4
|
import { AppExecutor, LibExecutor } from "../executors";
|
|
5
5
|
import { createWorkflowApplyReport, workflowCommandsForPlan } from "./artifacts";
|
|
6
|
-
import {
|
|
6
|
+
import { buildAkanModuleContextIndex } from "./moduleIndex";
|
|
7
|
+
import {
|
|
8
|
+
insertionIndexForFieldOrder,
|
|
9
|
+
inspectConstantStructure,
|
|
10
|
+
inspectDictionaryStructure,
|
|
11
|
+
moduleComponentName,
|
|
12
|
+
moduleSourcePaths,
|
|
13
|
+
normalizeFieldType,
|
|
14
|
+
} from "./source";
|
|
7
15
|
import type {
|
|
8
16
|
PrimitiveChangedFile,
|
|
9
17
|
PrimitiveGeneratedFile,
|
|
@@ -47,6 +55,14 @@ const postApplyDiagnostic = (code: string, message: string, target: string): Wor
|
|
|
47
55
|
context: { target },
|
|
48
56
|
});
|
|
49
57
|
|
|
58
|
+
const postApplyWarning = (code: string, message: string, target: string): WorkflowDiagnostic => ({
|
|
59
|
+
severity: "warning",
|
|
60
|
+
code,
|
|
61
|
+
message,
|
|
62
|
+
failureScope: "source-change",
|
|
63
|
+
context: { target },
|
|
64
|
+
});
|
|
65
|
+
|
|
50
66
|
const sourceKindForPath = (filePath: string) => {
|
|
51
67
|
if (filePath.endsWith(".tsx")) return ts.ScriptKind.TSX;
|
|
52
68
|
if (filePath.endsWith(".ts")) return ts.ScriptKind.TS;
|
|
@@ -81,7 +97,8 @@ const checkTypeScriptSyntax = async (workspace: Workspace, filePath: string) =>
|
|
|
81
97
|
if (!scriptKind) return null;
|
|
82
98
|
const content = await workspace.readFile(filePath);
|
|
83
99
|
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
84
|
-
const diagnostic = source.parseDiagnostics[
|
|
100
|
+
const diagnostic = ((source as ts.SourceFile & { parseDiagnostics?: readonly ts.DiagnosticWithLocation[] })
|
|
101
|
+
.parseDiagnostics ?? [])[0];
|
|
85
102
|
if (!diagnostic) return null;
|
|
86
103
|
const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
87
104
|
const location = position ? `:${position.line + 1}:${position.character + 1}` : "";
|
|
@@ -136,6 +153,188 @@ const checkRecommendationPath = async (
|
|
|
136
153
|
};
|
|
137
154
|
};
|
|
138
155
|
|
|
156
|
+
const sourceChangeError = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
157
|
+
diagnostics.some(
|
|
158
|
+
(diagnostic) =>
|
|
159
|
+
diagnostic.severity === "error" &&
|
|
160
|
+
(diagnostic.failureScope === "source-change" ||
|
|
161
|
+
!diagnostic.failureScope ||
|
|
162
|
+
diagnostic.failureScope === "unknown"),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const fieldCount = (fields: readonly string[], fieldName: string) =>
|
|
166
|
+
fields.filter((field) => field === fieldName).length;
|
|
167
|
+
|
|
168
|
+
const fieldOrderValid = (fields: readonly string[], fieldName: string) => {
|
|
169
|
+
const actualIndex = fields.indexOf(fieldName);
|
|
170
|
+
if (actualIndex < 0 || fields.lastIndexOf(fieldName) !== actualIndex) return false;
|
|
171
|
+
const fieldsWithoutRequested = fields.filter((_, index) => index !== actualIndex);
|
|
172
|
+
return insertionIndexForFieldOrder(fieldsWithoutRequested, fieldName) === actualIndex;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const workflowModuleContext = async (workspace: Workspace, plan: WorkflowPlan): Promise<AkanModuleContext | null> => {
|
|
176
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
177
|
+
const moduleName = workflowStringInput(plan.inputs.module);
|
|
178
|
+
if (!app || !moduleName) return null;
|
|
179
|
+
const [apps, libs] = await workspace.getSyss();
|
|
180
|
+
const sysType = apps.includes(app) ? "app" : libs.includes(app) ? "lib" : null;
|
|
181
|
+
if (!sysType) return null;
|
|
182
|
+
const modulePath = `${sysType}s/${app}/lib/${moduleName}`;
|
|
183
|
+
const files = await workspace.readdir(modulePath);
|
|
184
|
+
const abstractPath = `${modulePath}/${moduleName}.abstract.md`;
|
|
185
|
+
return {
|
|
186
|
+
kind: "domain",
|
|
187
|
+
name: moduleName,
|
|
188
|
+
folderName: moduleName,
|
|
189
|
+
sysName: app,
|
|
190
|
+
sysType,
|
|
191
|
+
path: modulePath,
|
|
192
|
+
abstract: {
|
|
193
|
+
path: abstractPath,
|
|
194
|
+
exists: files.includes(`${moduleName}.abstract.md`),
|
|
195
|
+
headings: [],
|
|
196
|
+
},
|
|
197
|
+
files,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const structureCheck = (
|
|
202
|
+
code: string,
|
|
203
|
+
target: string,
|
|
204
|
+
status: WorkflowPostApplyCheck["status"],
|
|
205
|
+
message: string,
|
|
206
|
+
): WorkflowPostApplyCheck => ({
|
|
207
|
+
code,
|
|
208
|
+
target,
|
|
209
|
+
status,
|
|
210
|
+
message,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const checkAddFieldStructure = async (workspace: Workspace, plan: WorkflowPlan): Promise<WorkflowStepResult> => {
|
|
214
|
+
if (plan.workflow !== "add-field" && plan.workflow !== "add-enum-field") return {};
|
|
215
|
+
const app = workflowStringInput(plan.inputs.app);
|
|
216
|
+
const moduleName = workflowStringInput(plan.inputs.module);
|
|
217
|
+
const fieldName = workflowStringInput(plan.inputs.field);
|
|
218
|
+
const typeName = workflowStringInput(plan.inputs.type);
|
|
219
|
+
if (!app || !moduleName || !fieldName || !typeName) return {};
|
|
220
|
+
|
|
221
|
+
const moduleContext = await workflowModuleContext(workspace, plan);
|
|
222
|
+
if (!moduleContext) return {};
|
|
223
|
+
|
|
224
|
+
const paths = moduleSourcePaths(moduleName);
|
|
225
|
+
const constantPath = `${moduleContext.path}/${paths.constant.replace(`lib/${moduleName}/`, "")}`;
|
|
226
|
+
const dictionaryPath = `${moduleContext.path}/${paths.dictionary.replace(`lib/${moduleName}/`, "")}`;
|
|
227
|
+
const moduleClassName = moduleComponentName(moduleName);
|
|
228
|
+
const inputClassName = `${moduleClassName}Input`;
|
|
229
|
+
const index = await buildAkanModuleContextIndex(workspace, moduleContext, { field: fieldName });
|
|
230
|
+
const diagnostics: WorkflowDiagnostic[] = [];
|
|
231
|
+
const postApplyChecks: WorkflowPostApplyCheck[] = [];
|
|
232
|
+
|
|
233
|
+
const constantContent = await workspace.readFile(constantPath);
|
|
234
|
+
const constantStructure = inspectConstantStructure(constantContent, inputClassName, moduleClassName);
|
|
235
|
+
const constantNames = constantStructure.fields.map((field) => field.name);
|
|
236
|
+
const requestedConstantFields = constantStructure.fields.filter((field) => field.name === fieldName);
|
|
237
|
+
const normalizedType = typeName.toLowerCase() === "enum" ? typeName : normalizeFieldType(typeName);
|
|
238
|
+
const constantFailures = [
|
|
239
|
+
!constantStructure.parseValid ? "constant file does not parse" : null,
|
|
240
|
+
!constantStructure.inputObjectFound ? `${inputClassName} via builder object was not found` : null,
|
|
241
|
+
requestedConstantFields.length !== 1
|
|
242
|
+
? `field "${fieldName}" appears ${requestedConstantFields.length} time(s) in ${inputClassName}`
|
|
243
|
+
: null,
|
|
244
|
+
constantStructure.builderName &&
|
|
245
|
+
requestedConstantFields[0]?.expressionBuilder &&
|
|
246
|
+
requestedConstantFields[0].expressionBuilder !== constantStructure.builderName
|
|
247
|
+
? `field "${fieldName}" uses builder "${requestedConstantFields[0].expressionBuilder}" instead of "${constantStructure.builderName}"`
|
|
248
|
+
: null,
|
|
249
|
+
!constantStructure.builderName ? `${inputClassName} via builder parameter was not found` : null,
|
|
250
|
+
(normalizedType === "Int" || normalizedType === "Float") && !constantStructure.baseImports.includes(normalizedType)
|
|
251
|
+
? `missing ${normalizedType} import from "akanjs/base"`
|
|
252
|
+
: null,
|
|
253
|
+
workflowBooleanInput(plan.inputs.includeInLight) === true &&
|
|
254
|
+
fieldCount(constantStructure.lightProjectionFields, fieldName) !== 1
|
|
255
|
+
? `field "${fieldName}" is not present exactly once in Light${moduleClassName}`
|
|
256
|
+
: null,
|
|
257
|
+
...index.diagnostics
|
|
258
|
+
.filter((diagnostic) => diagnostic.severity === "error" && diagnostic.context?.paths?.includes(constantPath))
|
|
259
|
+
.map((diagnostic) => diagnostic.message),
|
|
260
|
+
].filter((failure): failure is string => failure !== null);
|
|
261
|
+
const constantValid = constantFailures.length === 0;
|
|
262
|
+
postApplyChecks.push(
|
|
263
|
+
structureCheck(
|
|
264
|
+
constantValid ? "workflow-post-apply-constant-shape-valid" : "workflow-post-apply-structure-invalid",
|
|
265
|
+
constantPath,
|
|
266
|
+
constantValid ? "passed" : "failed",
|
|
267
|
+
constantValid
|
|
268
|
+
? `${inputClassName} keeps via structure, builder usage, imports, and requested field presence.`
|
|
269
|
+
: constantFailures.join(" "),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
if (!constantValid) {
|
|
273
|
+
diagnostics.push(
|
|
274
|
+
postApplyDiagnostic("workflow-post-apply-structure-invalid", constantFailures.join(" "), constantPath),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const dictionaryContent = await workspace.readFile(dictionaryPath);
|
|
279
|
+
const dictionaryStructure = inspectDictionaryStructure(dictionaryContent, moduleClassName);
|
|
280
|
+
const dictionaryFailures = [
|
|
281
|
+
!dictionaryStructure.parseValid ? "dictionary file does not parse" : null,
|
|
282
|
+
!dictionaryStructure.modelObjectFound ? `.model<${moduleClassName}> object was not found` : null,
|
|
283
|
+
dictionaryStructure.modelObjectFound && !dictionaryStructure.chainOrderValid
|
|
284
|
+
? `.model(), .slice(), .enum(), .error(), and .translate() chain order is broken: ${dictionaryStructure.chainMethods.join(
|
|
285
|
+
" -> ",
|
|
286
|
+
)}`
|
|
287
|
+
: null,
|
|
288
|
+
fieldCount(dictionaryStructure.fields, fieldName) !== 1
|
|
289
|
+
? `field "${fieldName}" appears ${fieldCount(dictionaryStructure.fields, fieldName)} time(s) in .model<${moduleClassName}>`
|
|
290
|
+
: null,
|
|
291
|
+
...index.diagnostics
|
|
292
|
+
.filter((diagnostic) => diagnostic.severity === "error" && diagnostic.context?.paths?.includes(dictionaryPath))
|
|
293
|
+
.map((diagnostic) => diagnostic.message),
|
|
294
|
+
].filter((failure): failure is string => failure !== null);
|
|
295
|
+
const dictionaryValid = dictionaryFailures.length === 0;
|
|
296
|
+
postApplyChecks.push(
|
|
297
|
+
structureCheck(
|
|
298
|
+
dictionaryValid ? "workflow-post-apply-dictionary-shape-valid" : "workflow-post-apply-structure-invalid",
|
|
299
|
+
dictionaryPath,
|
|
300
|
+
dictionaryValid ? "passed" : "failed",
|
|
301
|
+
dictionaryValid
|
|
302
|
+
? `.model<${moduleClassName}> keeps the requested field inside the model object and preserves dictionary chain order.`
|
|
303
|
+
: dictionaryFailures.join(" "),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
if (!dictionaryValid) {
|
|
307
|
+
diagnostics.push(
|
|
308
|
+
postApplyDiagnostic("workflow-post-apply-structure-invalid", dictionaryFailures.join(" "), dictionaryPath),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const constantOrderValid = fieldOrderValid(constantNames, fieldName);
|
|
313
|
+
const dictionaryOrderValid = fieldOrderValid(dictionaryStructure.fields, fieldName);
|
|
314
|
+
const orderValid = constantOrderValid && dictionaryOrderValid;
|
|
315
|
+
postApplyChecks.push(
|
|
316
|
+
structureCheck(
|
|
317
|
+
"workflow-post-apply-field-order-valid",
|
|
318
|
+
`${constantPath}, ${dictionaryPath}`,
|
|
319
|
+
orderValid ? "passed" : "failed",
|
|
320
|
+
orderValid
|
|
321
|
+
? `Field "${fieldName}" follows the shared priority ordering policy.`
|
|
322
|
+
: `Field "${fieldName}" is present but does not match the shared priority ordering policy.`,
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
if (!orderValid) {
|
|
326
|
+
diagnostics.push(
|
|
327
|
+
postApplyWarning(
|
|
328
|
+
"workflow-post-apply-field-order-mismatch",
|
|
329
|
+
`Field "${fieldName}" is present but does not match the shared priority ordering policy.`,
|
|
330
|
+
`${constantPath}, ${dictionaryPath}`,
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { diagnostics, postApplyChecks };
|
|
336
|
+
};
|
|
337
|
+
|
|
139
338
|
const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
140
339
|
if (!target) return null;
|
|
141
340
|
const [apps, libs] = await workspace.getSyss();
|
|
@@ -173,7 +372,7 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
|
|
|
173
372
|
const policy = addFieldUiPolicyForType(typeName ?? "String");
|
|
174
373
|
const surfaces = workflowStringArrayInput(plan.inputs.surfaces);
|
|
175
374
|
const templateRequested = surfaces?.includes("template") ?? false;
|
|
176
|
-
const moduleClassName =
|
|
375
|
+
const moduleClassName = moduleComponentName(module);
|
|
177
376
|
const target = `${app ? `apps/${app}` : "*"}/${moduleSourcePaths(module).template}`;
|
|
178
377
|
return {
|
|
179
378
|
recommendations: [
|
|
@@ -182,10 +381,10 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
|
|
|
182
381
|
kind: "manual-action",
|
|
183
382
|
target,
|
|
184
383
|
action: templateRequested
|
|
185
|
-
? `Template was requested for ${field}. If no Template file changed,
|
|
186
|
-
: `Template was not selected, so
|
|
384
|
+
? `Template was requested for ${field}. If no Template file changed, users will not see the field in the form yet because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Add it inside Layout.Template near existing Field components.`
|
|
385
|
+
: `Template was not selected, so users will not see ${field} in the form from this apply. If list/card display is needed, include ${field} in Light${moduleClassName} projection data and place it in the local Unit/View card layout.`,
|
|
187
386
|
confidence: "medium",
|
|
188
|
-
message: `Review UI
|
|
387
|
+
message: `Review user-visible UI for ${module}.${field}; recommended component is ${policy.component}.`,
|
|
189
388
|
},
|
|
190
389
|
],
|
|
191
390
|
nextActions: [
|
|
@@ -362,6 +561,11 @@ export class WorkflowExecutor {
|
|
|
362
561
|
postApplyChecks.push(...(result.postApplyChecks ?? []));
|
|
363
562
|
diagnostics.push(...(result.diagnostics ?? []));
|
|
364
563
|
}
|
|
564
|
+
if (!sourceChangeError(diagnostics)) {
|
|
565
|
+
const result = await checkAddFieldStructure(this.workspace, plan);
|
|
566
|
+
postApplyChecks.push(...(result.postApplyChecks ?? []));
|
|
567
|
+
diagnostics.push(...(result.diagnostics ?? []));
|
|
568
|
+
}
|
|
365
569
|
const recommendationDiagnostics = await Promise.all(
|
|
366
570
|
recommendations.map((recommendation) => checkRecommendationPath(this.workspace as Workspace, recommendation)),
|
|
367
571
|
);
|
package/workflow/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from "./artifacts";
|
|
2
2
|
export * from "./executor";
|
|
3
|
+
export * from "./moduleIndex";
|
|
3
4
|
export * from "./plan";
|
|
4
5
|
export * from "./primitive";
|
|
5
6
|
export * from "./render";
|
|
7
|
+
export * from "./rolloutGate";
|
|
6
8
|
export * from "./source";
|
|
7
9
|
export * from "./types";
|
|
8
10
|
export * from "./uiPolicy";
|