@akanjs/devkit 2.3.9-rc.8 → 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.ts +37 -6
- package/package.json +2 -2
- package/workflow/artifacts.ts +108 -13
- package/workflow/executor.ts +5 -5
- package/workflow/plan.ts +61 -7
- package/workflow/render.ts +45 -7
- package/workflow/rolloutGate.ts +9 -6
- package/workflow/types.ts +42 -2
package/akanMcpContract.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
WorkflowDiagnostic,
|
|
11
11
|
WorkflowPlanInputs,
|
|
12
12
|
} from "./workflow";
|
|
13
|
-
import { buildAkanModuleContextIndex, toolingRolloutGate } from "./workflow";
|
|
13
|
+
import { buildAkanModuleContextIndex, createWorkflowBaselineSummary, toolingRolloutGate } from "./workflow";
|
|
14
14
|
|
|
15
15
|
export type McpToolDefinition = {
|
|
16
16
|
name: string;
|
|
@@ -158,6 +158,8 @@ export const applyFirstPolicy = {
|
|
|
158
158
|
mode: "apply-first",
|
|
159
159
|
directSourceEdits: "fallback-only",
|
|
160
160
|
applyRequiredWhen: ["plan_workflow returns planPath", "plan_workflow returns next.tool=apply_workflow"],
|
|
161
|
+
approvalMeaning:
|
|
162
|
+
"requiresApproval is an agent/user review signal before apply_workflow mutates files; it is not a separate MCP permission gate.",
|
|
161
163
|
validationRequiredAfterApply: ["validationTarget", "applyReportPath"],
|
|
162
164
|
fallbackAllowedWhen: [
|
|
163
165
|
"list_workflows and explain_workflow show no matching workflow",
|
|
@@ -311,7 +313,12 @@ export const readonlyMcpTools: McpToolDefinition[] = [
|
|
|
311
313
|
description: "Report workspace diagnostics, optionally split into baseline and workflow scopes.",
|
|
312
314
|
inputSchema: {
|
|
313
315
|
type: "object",
|
|
314
|
-
properties: {
|
|
316
|
+
properties: {
|
|
317
|
+
strict: booleanProperty,
|
|
318
|
+
runIdOrPlan: stringProperty,
|
|
319
|
+
changedFiles: stringArrayProperty,
|
|
320
|
+
includeBaselineDetails: booleanProperty,
|
|
321
|
+
},
|
|
315
322
|
},
|
|
316
323
|
},
|
|
317
324
|
{
|
|
@@ -359,7 +366,7 @@ export const applyMcpTools: McpToolDefinition[] = [
|
|
|
359
366
|
description: "Validate a plan, apply report, validationTarget, or run artifact.",
|
|
360
367
|
inputSchema: {
|
|
361
368
|
type: "object",
|
|
362
|
-
properties: { runIdOrPlan: stringProperty },
|
|
369
|
+
properties: { runIdOrPlan: stringProperty, includeBaselineDetails: booleanProperty },
|
|
363
370
|
required: ["runIdOrPlan"],
|
|
364
371
|
},
|
|
365
372
|
},
|
|
@@ -409,7 +416,17 @@ export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
|
|
|
409
416
|
validationStatuses: {
|
|
410
417
|
sourceStatus: ["passed", "failed", "unknown"],
|
|
411
418
|
workspaceStatus: ["passed", "failed", "unknown"],
|
|
412
|
-
|
|
419
|
+
validationCommandsStatus: ["passed", "failed", "unknown"],
|
|
420
|
+
baselineStatus: ["passed", "failed", "unknown"],
|
|
421
|
+
overallStatus: [
|
|
422
|
+
"passed",
|
|
423
|
+
"failed",
|
|
424
|
+
"passed-with-baseline-blockers",
|
|
425
|
+
"blocked-by-workspace-config",
|
|
426
|
+
"blocked-by-environment",
|
|
427
|
+
],
|
|
428
|
+
baselineSummary:
|
|
429
|
+
"Default MCP validation output summarizes baseline diagnostics; pass includeBaselineDetails=true for full baselineDiagnostics.",
|
|
413
430
|
knownBlockers: "Repeated workspace-config/environment failures are summarized before command output.",
|
|
414
431
|
},
|
|
415
432
|
moduleContextInputs: {
|
|
@@ -420,6 +437,7 @@ export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
|
|
|
420
437
|
toolingRolloutGate,
|
|
421
438
|
directEditFallbackPolicy: applyFirstPolicy,
|
|
422
439
|
applyReportFields: [
|
|
440
|
+
"summary",
|
|
423
441
|
"appliedCommands",
|
|
424
442
|
"recommendedValidationCommands",
|
|
425
443
|
"commands",
|
|
@@ -484,8 +502,20 @@ export const inspectAkanContext = async (
|
|
|
484
502
|
strict: true,
|
|
485
503
|
runIdOrPlan: workspacePath(workspace, props.request.runIdOrPlan),
|
|
486
504
|
});
|
|
505
|
+
const baselineSummary = createWorkflowBaselineSummary(
|
|
506
|
+
(doctor.baselineDiagnostics ?? []).map(
|
|
507
|
+
(diagnostic): WorkflowDiagnostic => ({
|
|
508
|
+
severity: diagnostic.severity,
|
|
509
|
+
code: diagnostic.code,
|
|
510
|
+
message: diagnostic.message,
|
|
511
|
+
scope: diagnostic.scope,
|
|
512
|
+
context: diagnostic.context,
|
|
513
|
+
}),
|
|
514
|
+
),
|
|
515
|
+
{ detailsIncluded: false },
|
|
516
|
+
);
|
|
487
517
|
diagnostics.push(
|
|
488
|
-
...doctor.diagnostics.map(
|
|
518
|
+
...(doctor.workflowDiagnostics ?? doctor.diagnostics).map(
|
|
489
519
|
(diagnostic): WorkflowDiagnostic => ({
|
|
490
520
|
severity: diagnostic.severity,
|
|
491
521
|
code: diagnostic.code,
|
|
@@ -516,7 +546,8 @@ export const inspectAkanContext = async (
|
|
|
516
546
|
},
|
|
517
547
|
{
|
|
518
548
|
status: doctor.status,
|
|
519
|
-
|
|
549
|
+
baselineSummary,
|
|
550
|
+
baselineDiagnostics: 0,
|
|
520
551
|
workflowDiagnostics: doctor.workflowDiagnostics?.length ?? 0,
|
|
521
552
|
},
|
|
522
553
|
);
|
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,6 +7,7 @@ import type {
|
|
|
7
7
|
RepairReport,
|
|
8
8
|
WorkflowApplyCommand,
|
|
9
9
|
WorkflowApplyReport,
|
|
10
|
+
WorkflowBaselineSummary,
|
|
10
11
|
WorkflowDiagnostic,
|
|
11
12
|
WorkflowFailureScope,
|
|
12
13
|
WorkflowKnownBlocker,
|
|
@@ -30,6 +31,12 @@ const sourceChangeBlocked = (diagnostics: readonly WorkflowDiagnostic[]) =>
|
|
|
30
31
|
diagnostic.failureScope === "unknown"),
|
|
31
32
|
);
|
|
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
|
+
|
|
33
40
|
const inferNextActionCode = (
|
|
34
41
|
action: { action?: WorkflowNextActionCode; command: string },
|
|
35
42
|
diagnostics: readonly WorkflowDiagnostic[],
|
|
@@ -72,6 +79,7 @@ export const createWorkflowApplyReport = ({
|
|
|
72
79
|
| "applyReportPath"
|
|
73
80
|
| "validationTarget"
|
|
74
81
|
| "status"
|
|
82
|
+
| "summary"
|
|
75
83
|
| "appliedCommands"
|
|
76
84
|
| "recommendedValidationCommands"
|
|
77
85
|
| "commands"
|
|
@@ -99,13 +107,19 @@ export const createWorkflowApplyReport = ({
|
|
|
99
107
|
const orderedNextActions = uniqueBy(nextActionsWithIntent, (action) => action.command).sort(
|
|
100
108
|
(left, right) => nextActionPriority(left, diagnostics) - nextActionPriority(right, diagnostics),
|
|
101
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}`);
|
|
102
112
|
return {
|
|
103
113
|
schemaVersion: 1,
|
|
104
114
|
workflow,
|
|
105
115
|
mode,
|
|
106
116
|
status: workflowStatus(diagnostics),
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
summary: {
|
|
118
|
+
sourceFilesChanged,
|
|
119
|
+
generatedFilesSynced,
|
|
120
|
+
},
|
|
121
|
+
changedFiles: sourceFilesChanged,
|
|
122
|
+
generatedFiles: generatedFilesSynced,
|
|
109
123
|
appliedCommands: uniqueBy(appliedCommands, (command) => command.command),
|
|
110
124
|
recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
|
|
111
125
|
commands: uniqueBy(validationCommands, (command) => command.command),
|
|
@@ -246,6 +260,58 @@ const statusForValidationKind = (
|
|
|
246
260
|
return matching.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
247
261
|
};
|
|
248
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
|
+
|
|
249
315
|
const createKnownBlockers = (
|
|
250
316
|
commands: readonly WorkflowValidationCommandResult[],
|
|
251
317
|
diagnostics: readonly WorkflowDiagnostic[],
|
|
@@ -292,20 +358,37 @@ const createKnownBlockers = (
|
|
|
292
358
|
|
|
293
359
|
const createValidationStatuses = (
|
|
294
360
|
commands: readonly WorkflowValidationCommandResult[],
|
|
295
|
-
|
|
361
|
+
reportDiagnostics: readonly WorkflowDiagnostic[],
|
|
362
|
+
baselineDiagnostics: readonly WorkflowDiagnostic[],
|
|
363
|
+
workflowDiagnostics: readonly WorkflowDiagnostic[],
|
|
296
364
|
): {
|
|
297
365
|
sourceStatus: WorkflowValidationStatus;
|
|
298
366
|
workspaceStatus: WorkflowValidationStatus;
|
|
367
|
+
validationCommandsStatus: WorkflowValidationStatus;
|
|
368
|
+
baselineStatus: WorkflowValidationStatus;
|
|
299
369
|
overallStatus: WorkflowOverallStatus;
|
|
300
370
|
summary: {
|
|
301
371
|
sourceChange: WorkflowValidationStatus;
|
|
302
372
|
generatedSync: WorkflowValidationStatus;
|
|
373
|
+
validationCommands: WorkflowValidationStatus;
|
|
374
|
+
baseline: WorkflowValidationStatus;
|
|
303
375
|
workspaceConfig: WorkflowValidationStatus;
|
|
304
376
|
environment: WorkflowValidationStatus;
|
|
305
377
|
};
|
|
306
378
|
} => {
|
|
379
|
+
const diagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
307
380
|
const scopes = [...failedCommandScopes(commands), ...errorDiagnosticScopes(diagnostics)];
|
|
308
|
-
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];
|
|
309
392
|
const workspaceStatus =
|
|
310
393
|
hasScopeFailure(scopes, "workspace-config", diagnostics) || hasScopeFailure(scopes, "environment", diagnostics)
|
|
311
394
|
? "failed"
|
|
@@ -314,20 +397,28 @@ const createValidationStatuses = (
|
|
|
314
397
|
: "unknown";
|
|
315
398
|
const overallStatus = hasScopeFailure(scopes, "source-change", diagnostics)
|
|
316
399
|
? "failed"
|
|
317
|
-
:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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";
|
|
324
411
|
return {
|
|
325
412
|
sourceStatus,
|
|
326
413
|
workspaceStatus,
|
|
414
|
+
validationCommandsStatus,
|
|
415
|
+
baselineStatus,
|
|
327
416
|
overallStatus,
|
|
328
417
|
summary: {
|
|
329
418
|
sourceChange: sourceStatus,
|
|
330
419
|
generatedSync: statusForValidationKind(commands, "sync"),
|
|
420
|
+
validationCommands: validationCommandsStatus,
|
|
421
|
+
baseline: baselineStatus,
|
|
331
422
|
workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
|
|
332
423
|
environment: statusForScope(commands, diagnostics, scopes, "environment"),
|
|
333
424
|
},
|
|
@@ -377,7 +468,8 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
377
468
|
);
|
|
378
469
|
const reportDiagnostics = [...diagnostics, ...commandDiagnostics];
|
|
379
470
|
const scopedDiagnostics = [...reportDiagnostics, ...baselineDiagnostics, ...workflowDiagnostics];
|
|
380
|
-
const
|
|
471
|
+
const knownBlockers = createKnownBlockers(results, scopedDiagnostics);
|
|
472
|
+
const statuses = createValidationStatuses(results, reportDiagnostics, baselineDiagnostics, workflowDiagnostics);
|
|
381
473
|
return {
|
|
382
474
|
schemaVersion: 1,
|
|
383
475
|
runId,
|
|
@@ -386,9 +478,12 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
386
478
|
source,
|
|
387
479
|
status: statuses.overallStatus === "passed" ? "passed" : "failed",
|
|
388
480
|
...statuses,
|
|
389
|
-
knownBlockers
|
|
481
|
+
knownBlockers,
|
|
390
482
|
commands: results,
|
|
391
483
|
diagnostics: reportDiagnostics,
|
|
484
|
+
baselineSummary: createWorkflowBaselineSummary(baselineDiagnostics, {
|
|
485
|
+
knownBlockerCount: knownBlockers.filter((blocker) => blocker.failureScope === "workspace-config").length,
|
|
486
|
+
}),
|
|
392
487
|
baselineDiagnostics,
|
|
393
488
|
workflowDiagnostics,
|
|
394
489
|
repairActions: uniqueBy(repairActions, (action) => action.command),
|
package/workflow/executor.ts
CHANGED
|
@@ -97,8 +97,8 @@ const checkTypeScriptSyntax = async (workspace: Workspace, filePath: string) =>
|
|
|
97
97
|
if (!scriptKind) return null;
|
|
98
98
|
const content = await workspace.readFile(filePath);
|
|
99
99
|
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
100
|
-
const diagnostic =
|
|
101
|
-
|
|
100
|
+
const diagnostic = ((source as ts.SourceFile & { parseDiagnostics?: readonly ts.DiagnosticWithLocation[] })
|
|
101
|
+
.parseDiagnostics ?? [])[0];
|
|
102
102
|
if (!diagnostic) return null;
|
|
103
103
|
const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
104
104
|
const location = position ? `:${position.line + 1}:${position.character + 1}` : "";
|
|
@@ -381,10 +381,10 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
|
|
|
381
381
|
kind: "manual-action",
|
|
382
382
|
target,
|
|
383
383
|
action: templateRequested
|
|
384
|
-
? `Template was requested for ${field}. If no Template file changed,
|
|
385
|
-
: `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.`,
|
|
386
386
|
confidence: "medium",
|
|
387
|
-
message: `Review UI
|
|
387
|
+
message: `Review user-visible UI for ${module}.${field}; recommended component is ${policy.component}.`,
|
|
388
388
|
},
|
|
389
389
|
],
|
|
390
390
|
nextActions: [
|
package/workflow/plan.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { capitalize } from "akanjs/common";
|
|
2
|
+
import { workflowPlanApproval } from "./artifacts";
|
|
2
3
|
import { coerceFieldDefault, moduleSourcePaths } from "./source";
|
|
3
4
|
import type {
|
|
4
5
|
WorkflowDiagnostic,
|
|
@@ -91,11 +92,59 @@ const createAddFieldDefaultRecommendations = (inputs: Record<string, WorkflowInp
|
|
|
91
92
|
kind: "manual-action",
|
|
92
93
|
confidence: "high",
|
|
93
94
|
message: `Default for ${field} will be normalized to ${coercion.normalizedType} literal ${coercion.expression}.`,
|
|
94
|
-
action:
|
|
95
|
+
action:
|
|
96
|
+
"Review the normalized default in the apply report. To leave the field without a default, omit the default input.",
|
|
95
97
|
},
|
|
96
98
|
];
|
|
97
99
|
};
|
|
98
100
|
|
|
101
|
+
const moneyLikeFieldPattern = /(budget|price|amount|cost|rate|fee|salary|balance)/i;
|
|
102
|
+
const wholeNumberFieldPattern = /(count|quantity|total|number|index|rank|order|age)/i;
|
|
103
|
+
|
|
104
|
+
const createAddFieldInputRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
|
|
105
|
+
const field = typeof inputs.field === "string" ? inputs.field : "<field>";
|
|
106
|
+
const typeName = typeof inputs.type === "string" ? inputs.type : null;
|
|
107
|
+
const recommendations: WorkflowRecommendation[] = [];
|
|
108
|
+
if (!typeName) return recommendations;
|
|
109
|
+
if (typeName.toLowerCase() === "number" || typeName.toLowerCase() === "numeric") {
|
|
110
|
+
const recommendedType = moneyLikeFieldPattern.test(field)
|
|
111
|
+
? "Float"
|
|
112
|
+
: wholeNumberFieldPattern.test(field)
|
|
113
|
+
? "Int"
|
|
114
|
+
: null;
|
|
115
|
+
recommendations.push({
|
|
116
|
+
code: "add-field-type-choice",
|
|
117
|
+
kind: "input-guidance",
|
|
118
|
+
confidence: recommendedType ? "high" : "medium",
|
|
119
|
+
message: recommendedType
|
|
120
|
+
? `Use ${recommendedType} for ${field}; Float is recommended for money-like decimal values and Int for whole-number values.`
|
|
121
|
+
: `Choose Int for whole-number ${field} values or Float for decimal ${field} values.`,
|
|
122
|
+
action: recommendedType
|
|
123
|
+
? `Re-run plan_workflow with type="${recommendedType}".`
|
|
124
|
+
: 'Re-run plan_workflow with type="Int" or type="Float".',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (typeName.toLowerCase() === "enum" && !Array.isArray(inputs.values)) {
|
|
128
|
+
recommendations.push({
|
|
129
|
+
code: "add-field-enum-values",
|
|
130
|
+
kind: "input-guidance",
|
|
131
|
+
confidence: "high",
|
|
132
|
+
message: `Enum field ${field} needs values before apply can choose valid dictionary and default behavior.`,
|
|
133
|
+
action: 'Pass values as an array or comma-separated string, for example values=["draft","done"].',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (inputs.default === undefined) {
|
|
137
|
+
recommendations.push({
|
|
138
|
+
code: "add-field-default-optional",
|
|
139
|
+
kind: "input-guidance",
|
|
140
|
+
confidence: "medium",
|
|
141
|
+
message: `No default will be written for ${field} because default is omitted.`,
|
|
142
|
+
action: "Keep default omitted when the field should have no default value.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return recommendations;
|
|
146
|
+
};
|
|
147
|
+
|
|
99
148
|
const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue>): WorkflowRecommendation[] => {
|
|
100
149
|
const app = typeof inputs.app === "string" ? inputs.app : "<app-or-lib>";
|
|
101
150
|
const module = typeof inputs.module === "string" ? inputs.module : "<module>";
|
|
@@ -163,9 +212,11 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
|
|
|
163
212
|
code: "add-field-light-projection-choice",
|
|
164
213
|
kind: "manual-action" as const,
|
|
165
214
|
target: paths.constant,
|
|
166
|
-
action: `Pass includeInLight=true when ${field}
|
|
215
|
+
action: `Pass includeInLight=true when users should see ${field} in list/card data. Without it, the field can exist on ${capitalize(
|
|
216
|
+
module,
|
|
217
|
+
)}Input but stay absent from Light${capitalize(module)} projections.`,
|
|
167
218
|
confidence: "medium" as const,
|
|
168
|
-
message:
|
|
219
|
+
message: `${module}.${field} is not selected for list/card projection data.`,
|
|
169
220
|
},
|
|
170
221
|
]
|
|
171
222
|
: []),
|
|
@@ -187,7 +238,8 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
|
|
|
187
238
|
kind: "manual-action" as const,
|
|
188
239
|
target: paths.template,
|
|
189
240
|
confidence: "medium" as const,
|
|
190
|
-
message: `Template
|
|
241
|
+
message: `Template form auto-edit is skipped for ${module}.${field} because surfaces does not include "template".`,
|
|
242
|
+
action: `Pass surfaces=["template"] when users should enter ${field} in the Template form.`,
|
|
191
243
|
},
|
|
192
244
|
]
|
|
193
245
|
: []),
|
|
@@ -195,13 +247,14 @@ const createAddFieldRecommendations = (inputs: Record<string, WorkflowInputValue
|
|
|
195
247
|
code: "add-field-ui-manual-review",
|
|
196
248
|
kind: "manual-action",
|
|
197
249
|
target: paths.template,
|
|
198
|
-
action: `
|
|
250
|
+
action: `After apply, verify what users can see: Template form auto-edit only runs when a generated ${module}Form hook and Layout.Template field list are present. Unit/View cards are not auto-edited, so ${field} may be stored on ${capitalize(
|
|
199
251
|
module,
|
|
200
|
-
)}
|
|
252
|
+
)} but not shown in list/card UI until you place it in the local card layout.`,
|
|
201
253
|
confidence: "medium",
|
|
202
|
-
message:
|
|
254
|
+
message: `${app}:${module}.${field} may need UI review for user-visible Template, Unit, and View behavior.`,
|
|
203
255
|
},
|
|
204
256
|
...createAddFieldDefaultRecommendations(inputs),
|
|
257
|
+
...createAddFieldInputRecommendations(inputs),
|
|
205
258
|
];
|
|
206
259
|
};
|
|
207
260
|
|
|
@@ -371,5 +424,6 @@ export const createWorkflowPlan = (spec: WorkflowSpec, rawInputs: Record<string,
|
|
|
371
424
|
diagnostics,
|
|
372
425
|
recommendations: createWorkflowPlanRecommendations(spec, inputs),
|
|
373
426
|
requiresApproval: true,
|
|
427
|
+
approval: workflowPlanApproval,
|
|
374
428
|
};
|
|
375
429
|
};
|
package/workflow/render.ts
CHANGED
|
@@ -51,6 +51,7 @@ export const renderWorkflowPlan = (plan: WorkflowPlan) =>
|
|
|
51
51
|
"",
|
|
52
52
|
`- Mode: ${plan.mode}`,
|
|
53
53
|
`- Requires approval: ${plan.requiresApproval}`,
|
|
54
|
+
`- Approval: ${plan.approval?.meaning ?? "Review this read-only plan before applying workflow mutations."}`,
|
|
54
55
|
"",
|
|
55
56
|
"## Inputs",
|
|
56
57
|
...Object.entries(plan.inputs).map(
|
|
@@ -108,6 +109,10 @@ const applySourceStatus = (report: WorkflowApplyReport) =>
|
|
|
108
109
|
: "passed";
|
|
109
110
|
|
|
110
111
|
export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
|
|
112
|
+
const applySummary = {
|
|
113
|
+
sourceFilesChanged: report.summary?.sourceFilesChanged ?? report.changedFiles,
|
|
114
|
+
generatedFilesSynced: report.summary?.generatedFilesSynced ?? report.generatedFiles,
|
|
115
|
+
};
|
|
111
116
|
const manualReviewItems = [
|
|
112
117
|
...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
|
|
113
118
|
...report.recommendations
|
|
@@ -137,6 +142,8 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
|
|
|
137
142
|
`- Status: ${report.status}`,
|
|
138
143
|
`- Source-change status: ${applySourceStatus(report)}`,
|
|
139
144
|
`- Workspace status: ${validationBlockers.length ? "blocked" : "not blocked during apply"}`,
|
|
145
|
+
`- Source files changed: ${applySummary.sourceFilesChanged.length}`,
|
|
146
|
+
`- Generated files queued for sync: ${applySummary.generatedFilesSynced.length}`,
|
|
140
147
|
...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
|
|
141
148
|
"",
|
|
142
149
|
"## Apply Checks",
|
|
@@ -148,13 +155,13 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
|
|
|
148
155
|
...(sourceBlockers.length ? sourceBlockers : ["- none"]),
|
|
149
156
|
"",
|
|
150
157
|
"## Automatically Modified",
|
|
151
|
-
...(
|
|
152
|
-
?
|
|
158
|
+
...(applySummary.sourceFilesChanged.length
|
|
159
|
+
? applySummary.sourceFilesChanged.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
153
160
|
: ["- none"]),
|
|
154
161
|
"",
|
|
155
162
|
"## Generated Sync",
|
|
156
|
-
...(
|
|
157
|
-
?
|
|
163
|
+
...(applySummary.generatedFilesSynced.length
|
|
164
|
+
? applySummary.generatedFilesSynced.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
|
|
158
165
|
: ["- none"]),
|
|
159
166
|
"",
|
|
160
167
|
"## Applied Commands",
|
|
@@ -192,19 +199,32 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
|
|
|
192
199
|
export const renderWorkflowApply = (report: WorkflowApplyReport, format: WorkflowFormat = "markdown") =>
|
|
193
200
|
format === "json" ? jsonText(report) : renderWorkflowApplyReport(report);
|
|
194
201
|
|
|
195
|
-
export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) =>
|
|
196
|
-
|
|
202
|
+
export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunReport) => {
|
|
203
|
+
const baselineSummary = report.baselineSummary ?? {
|
|
204
|
+
status: report.baselineDiagnostics?.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "unknown",
|
|
205
|
+
total: report.baselineDiagnostics?.length ?? 0,
|
|
206
|
+
totalErrors: report.baselineDiagnostics?.filter((diagnostic) => diagnostic.severity === "error").length ?? 0,
|
|
207
|
+
totalWarnings: report.baselineDiagnostics?.filter((diagnostic) => diagnostic.severity === "warning").length ?? 0,
|
|
208
|
+
detailsIncluded: true,
|
|
209
|
+
knownBlockerCount: report.knownBlockers.filter((blocker) => blocker.known).length,
|
|
210
|
+
byCode: [],
|
|
211
|
+
};
|
|
212
|
+
return [
|
|
197
213
|
`# Workflow Validation: ${report.workflow}`,
|
|
198
214
|
"",
|
|
199
215
|
`- Run: ${report.runId}`,
|
|
200
216
|
`- Status: ${report.status}`,
|
|
201
217
|
`- Source status: ${report.sourceStatus}`,
|
|
202
218
|
`- Workspace status: ${report.workspaceStatus}`,
|
|
219
|
+
`- Validation commands status: ${report.validationCommandsStatus ?? "unknown"}`,
|
|
220
|
+
`- Baseline status: ${report.baselineStatus ?? baselineSummary.status}`,
|
|
203
221
|
`- Overall status: ${report.overallStatus}`,
|
|
204
222
|
"",
|
|
205
223
|
"## Status Summary",
|
|
206
224
|
`- Source-change: ${report.summary.sourceChange}`,
|
|
207
225
|
`- Generated sync: ${report.summary.generatedSync}`,
|
|
226
|
+
`- Validation commands: ${report.summary.validationCommands ?? "unknown"}`,
|
|
227
|
+
`- Baseline: ${report.summary.baseline ?? baselineSummary.status}`,
|
|
208
228
|
`- Workspace config: ${report.summary.workspaceConfig}`,
|
|
209
229
|
`- Environment: ${report.summary.environment}`,
|
|
210
230
|
"",
|
|
@@ -216,11 +236,28 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
|
|
|
216
236
|
: ["- none"]),
|
|
217
237
|
"",
|
|
218
238
|
"## Existing Workspace Blockers",
|
|
239
|
+
`- Status: ${baselineSummary.status}`,
|
|
240
|
+
`- Errors: ${baselineSummary.totalErrors}`,
|
|
241
|
+
`- Warnings: ${baselineSummary.totalWarnings}`,
|
|
242
|
+
...(baselineSummary.byCode.length
|
|
243
|
+
? baselineSummary.byCode.map(
|
|
244
|
+
(item) => `- [${item.severity}] ${item.code} (${item.count}x): ${item.sampleMessage}`,
|
|
245
|
+
)
|
|
246
|
+
: ["- none"]),
|
|
247
|
+
...(baselineSummary.detailsIncluded
|
|
248
|
+
? []
|
|
249
|
+
: [
|
|
250
|
+
"- Baseline details are summarized by default. Re-run validation with includeBaselineDetails=true for full baselineDiagnostics.",
|
|
251
|
+
]),
|
|
252
|
+
"",
|
|
253
|
+
"## Existing Workspace Blocker Details",
|
|
219
254
|
...(report.baselineDiagnostics?.length
|
|
220
255
|
? report.baselineDiagnostics.map(
|
|
221
256
|
(diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
|
|
222
257
|
)
|
|
223
|
-
:
|
|
258
|
+
: baselineSummary.detailsIncluded
|
|
259
|
+
? ["- none"]
|
|
260
|
+
: ["- omitted"]),
|
|
224
261
|
"",
|
|
225
262
|
"## Known Blockers",
|
|
226
263
|
...(report.knownBlockers.length
|
|
@@ -256,6 +293,7 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
|
|
|
256
293
|
: ["- none"]),
|
|
257
294
|
"",
|
|
258
295
|
].join("\n");
|
|
296
|
+
};
|
|
259
297
|
|
|
260
298
|
export const renderWorkflowValidation = (report: WorkflowValidationRunReport, format: WorkflowFormat = "markdown") =>
|
|
261
299
|
format === "json" ? jsonText(report) : renderWorkflowValidationRunReport(report);
|
package/workflow/rolloutGate.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface ToolingRolloutViolation {
|
|
|
23
23
|
reason: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
type RestrictedToolingRolloutCandidate = ToolingRolloutCandidate & {
|
|
27
|
+
status: Exclude<ToolingRolloutCandidateStatus, "allowed">;
|
|
28
|
+
};
|
|
29
|
+
|
|
26
30
|
const dependencySections = [
|
|
27
31
|
"dependencies",
|
|
28
32
|
"devDependencies",
|
|
@@ -90,14 +94,13 @@ export const toolingRolloutGate = {
|
|
|
90
94
|
candidates: toolingRolloutCandidates,
|
|
91
95
|
};
|
|
92
96
|
|
|
93
|
-
const isRestrictedCandidate = (
|
|
94
|
-
candidate: ToolingRolloutCandidate,
|
|
95
|
-
): candidate is ToolingRolloutCandidate & { status: Exclude<ToolingRolloutCandidateStatus, "allowed"> } =>
|
|
97
|
+
const isRestrictedCandidate = (candidate: ToolingRolloutCandidate): candidate is RestrictedToolingRolloutCandidate =>
|
|
96
98
|
candidate.status !== "allowed";
|
|
97
99
|
|
|
98
|
-
const restrictedCandidates = new Map(
|
|
99
|
-
|
|
100
|
-
);
|
|
100
|
+
const restrictedCandidates = new Map<string, RestrictedToolingRolloutCandidate>();
|
|
101
|
+
for (const candidate of toolingRolloutCandidates) {
|
|
102
|
+
if (isRestrictedCandidate(candidate)) restrictedCandidates.set(candidate.packageName, candidate);
|
|
103
|
+
}
|
|
101
104
|
|
|
102
105
|
const blockedTypeScriptVersion = (version: string) =>
|
|
103
106
|
/(?:^|[^\w])(?:rc|next|beta|canary|insiders)(?:[^\w]|$)/i.test(version) || /typescript@rc/i.test(version);
|
package/workflow/types.ts
CHANGED
|
@@ -14,10 +14,17 @@ export type WorkflowValidationStatus = "passed" | "failed" | "unknown";
|
|
|
14
14
|
export interface WorkflowValidationSummary {
|
|
15
15
|
sourceChange: WorkflowValidationStatus;
|
|
16
16
|
generatedSync: WorkflowValidationStatus;
|
|
17
|
+
validationCommands: WorkflowValidationStatus;
|
|
18
|
+
baseline: WorkflowValidationStatus;
|
|
17
19
|
workspaceConfig: WorkflowValidationStatus;
|
|
18
20
|
environment: WorkflowValidationStatus;
|
|
19
21
|
}
|
|
20
|
-
export type WorkflowOverallStatus =
|
|
22
|
+
export type WorkflowOverallStatus =
|
|
23
|
+
| "passed"
|
|
24
|
+
| "failed"
|
|
25
|
+
| "passed-with-baseline-blockers"
|
|
26
|
+
| "blocked-by-workspace-config"
|
|
27
|
+
| "blocked-by-environment";
|
|
21
28
|
|
|
22
29
|
export interface PrimitiveTargetInput {
|
|
23
30
|
app: string | null;
|
|
@@ -102,12 +109,19 @@ export interface WorkflowDiagnostic {
|
|
|
102
109
|
export interface WorkflowRecommendation {
|
|
103
110
|
code: string;
|
|
104
111
|
message: string;
|
|
105
|
-
kind: "auto-apply" | "validation" | "manual-action" | "import" | "placement" | "ui-component";
|
|
112
|
+
kind: "auto-apply" | "validation" | "manual-action" | "input-guidance" | "import" | "placement" | "ui-component";
|
|
106
113
|
target?: string;
|
|
107
114
|
action?: string;
|
|
108
115
|
confidence?: "high" | "medium" | "low";
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
export interface WorkflowPlanApproval {
|
|
119
|
+
required: true;
|
|
120
|
+
meaning: string;
|
|
121
|
+
applyTool: "apply_workflow";
|
|
122
|
+
canApplyWith?: Record<string, string>;
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
export interface AkanSourceSpan {
|
|
112
126
|
file: string;
|
|
113
127
|
startLine: number;
|
|
@@ -247,6 +261,7 @@ export interface WorkflowPlan {
|
|
|
247
261
|
diagnostics: WorkflowDiagnostic[];
|
|
248
262
|
recommendations: WorkflowRecommendation[];
|
|
249
263
|
requiresApproval: true;
|
|
264
|
+
approval?: WorkflowPlanApproval;
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
export interface WorkflowReport {
|
|
@@ -291,6 +306,24 @@ export interface WorkflowKnownBlocker {
|
|
|
291
306
|
known?: boolean;
|
|
292
307
|
}
|
|
293
308
|
|
|
309
|
+
export interface WorkflowBaselineSummaryCode {
|
|
310
|
+
code: string;
|
|
311
|
+
severity: "warning" | "error" | "mixed";
|
|
312
|
+
count: number;
|
|
313
|
+
sampleMessage: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface WorkflowBaselineSummary {
|
|
317
|
+
status: WorkflowValidationStatus;
|
|
318
|
+
total: number;
|
|
319
|
+
totalErrors: number;
|
|
320
|
+
totalWarnings: number;
|
|
321
|
+
detailsIncluded: boolean;
|
|
322
|
+
knownBlockerCount: number;
|
|
323
|
+
byCode: WorkflowBaselineSummaryCode[];
|
|
324
|
+
contextPaths?: string[];
|
|
325
|
+
}
|
|
326
|
+
|
|
294
327
|
export interface RepairAction {
|
|
295
328
|
command: string;
|
|
296
329
|
reason: string;
|
|
@@ -351,6 +384,10 @@ export interface WorkflowApplyReport {
|
|
|
351
384
|
workflow: string;
|
|
352
385
|
mode: "dry-run" | "apply";
|
|
353
386
|
status: "passed" | "failed";
|
|
387
|
+
summary: {
|
|
388
|
+
sourceFilesChanged: PrimitiveChangedFile[];
|
|
389
|
+
generatedFilesSynced: PrimitiveGeneratedFile[];
|
|
390
|
+
};
|
|
354
391
|
changedFiles: PrimitiveChangedFile[];
|
|
355
392
|
generatedFiles: PrimitiveGeneratedFile[];
|
|
356
393
|
appliedCommands: WorkflowApplyCommand[];
|
|
@@ -372,11 +409,14 @@ export interface WorkflowValidationRunReport {
|
|
|
372
409
|
status: "passed" | "failed";
|
|
373
410
|
sourceStatus: WorkflowValidationStatus;
|
|
374
411
|
workspaceStatus: WorkflowValidationStatus;
|
|
412
|
+
validationCommandsStatus: WorkflowValidationStatus;
|
|
413
|
+
baselineStatus: WorkflowValidationStatus;
|
|
375
414
|
summary: WorkflowValidationSummary;
|
|
376
415
|
overallStatus: WorkflowOverallStatus;
|
|
377
416
|
knownBlockers: WorkflowKnownBlocker[];
|
|
378
417
|
commands: WorkflowValidationCommandResult[];
|
|
379
418
|
diagnostics: WorkflowDiagnostic[];
|
|
419
|
+
baselineSummary: WorkflowBaselineSummary;
|
|
380
420
|
baselineDiagnostics?: WorkflowDiagnostic[];
|
|
381
421
|
workflowDiagnostics?: WorkflowDiagnostic[];
|
|
382
422
|
repairActions: RepairAction[];
|