@akanjs/devkit 2.3.9-rc.6 → 2.3.9-rc.8
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 +670 -0
- package/index.ts +1 -0
- package/package.json +2 -2
- package/workflow/artifacts.ts +58 -6
- package/workflow/executor.ts +208 -4
- package/workflow/index.ts +2 -0
- package/workflow/moduleIndex.test.ts +296 -0
- package/workflow/moduleIndex.ts +504 -0
- package/workflow/render.ts +4 -3
- package/workflow/rolloutGate.test.ts +109 -0
- package/workflow/rolloutGate.ts +138 -0
- package/workflow/source.test.ts +62 -0
- package/workflow/source.ts +463 -75
- package/workflow/types.ts +130 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createAkanValidationContract } from "./akanMcpContract";
|
|
3
|
+
|
|
4
|
+
describe("createAkanValidationContract", () => {
|
|
5
|
+
test("exposes reference-first tooling rollout gate semantics", () => {
|
|
6
|
+
const contract = createAkanValidationContract();
|
|
7
|
+
|
|
8
|
+
expect(contract.toolingRolloutGate).toMatchObject({
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
strategy: "reference-first-dependency-later",
|
|
11
|
+
});
|
|
12
|
+
expect(contract.toolingRolloutGate.candidates).toContainEqual(
|
|
13
|
+
expect.objectContaining({
|
|
14
|
+
packageName: "typescript",
|
|
15
|
+
status: "allowed",
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
expect(contract.toolingRolloutGate.candidates).toContainEqual(
|
|
19
|
+
expect.objectContaining({
|
|
20
|
+
packageName: "@ttsc/graph",
|
|
21
|
+
status: "reference-only",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("keeps dependency adoption gated by package and source-body constraints", () => {
|
|
27
|
+
const contract = createAkanValidationContract();
|
|
28
|
+
|
|
29
|
+
expect(contract.toolingRolloutGate.gateConditions).toContain(
|
|
30
|
+
"Works in Bun runtime and published package artifacts.",
|
|
31
|
+
);
|
|
32
|
+
expect(contract.toolingRolloutGate.gateConditions).toContain("Does not break dist/pkgs package verification.");
|
|
33
|
+
expect(contract.toolingRolloutGate.gateConditions).toContain(
|
|
34
|
+
"Does not move MCP responses toward returning source bodies.",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AkanContextAnalyzer, type AkanModuleContext } from "./akanContext";
|
|
3
|
+
import type { Workspace } from "./commandDecorators";
|
|
4
|
+
import type {
|
|
5
|
+
AkanContextEvidence,
|
|
6
|
+
AkanContextNext,
|
|
7
|
+
InspectAkanContextProps,
|
|
8
|
+
InspectAkanContextRequest,
|
|
9
|
+
InspectAkanContextResult,
|
|
10
|
+
WorkflowDiagnostic,
|
|
11
|
+
WorkflowPlanInputs,
|
|
12
|
+
} from "./workflow";
|
|
13
|
+
import { buildAkanModuleContextIndex, toolingRolloutGate } from "./workflow";
|
|
14
|
+
|
|
15
|
+
export type McpToolDefinition = {
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object";
|
|
20
|
+
properties: Record<string, unknown>;
|
|
21
|
+
required?: string[];
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const emptySchema = { type: "object" as const, properties: {} };
|
|
27
|
+
const stringProperty = { type: "string" };
|
|
28
|
+
const booleanProperty = { type: "boolean" };
|
|
29
|
+
const objectProperty = { type: "object", additionalProperties: true };
|
|
30
|
+
const stringArrayProperty = { type: "array", items: stringProperty };
|
|
31
|
+
const inspectAkanContextRequestTypes = [
|
|
32
|
+
"workspaceOverview",
|
|
33
|
+
"moduleContext",
|
|
34
|
+
"fieldInsertionContext",
|
|
35
|
+
"workflowDiagnostics",
|
|
36
|
+
"escape",
|
|
37
|
+
] as const;
|
|
38
|
+
const inspectAkanContextRequestTypeProperty = { type: "string", enum: inspectAkanContextRequestTypes };
|
|
39
|
+
const inspectAkanContextRequestBranches = [
|
|
40
|
+
{
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: { type: { const: "workspaceOverview" } },
|
|
43
|
+
required: ["type"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: { type: { const: "moduleContext" }, app: stringProperty, module: stringProperty },
|
|
48
|
+
required: ["type", "app", "module"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
type: { const: "fieldInsertionContext" },
|
|
54
|
+
app: stringProperty,
|
|
55
|
+
module: stringProperty,
|
|
56
|
+
field: stringProperty,
|
|
57
|
+
fieldType: stringProperty,
|
|
58
|
+
},
|
|
59
|
+
required: ["type", "app", "module", "field", "fieldType"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: { type: { const: "workflowDiagnostics" }, runIdOrPlan: stringProperty },
|
|
64
|
+
required: ["type", "runIdOrPlan"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: { type: { const: "escape" }, reason: stringProperty, nextStep: stringProperty },
|
|
69
|
+
required: ["type", "reason"],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
const inspectAkanContextInputSchema = {
|
|
73
|
+
type: "object" as const,
|
|
74
|
+
properties: {
|
|
75
|
+
question: {
|
|
76
|
+
...stringProperty,
|
|
77
|
+
description: "The user-facing question the agent is trying to answer before reading source bodies.",
|
|
78
|
+
},
|
|
79
|
+
draft: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
reason: stringProperty,
|
|
83
|
+
type: inspectAkanContextRequestTypeProperty,
|
|
84
|
+
},
|
|
85
|
+
required: ["reason", "type"],
|
|
86
|
+
},
|
|
87
|
+
review: {
|
|
88
|
+
...stringProperty,
|
|
89
|
+
description: "A short self-review explaining why this minimal request is sufficient.",
|
|
90
|
+
},
|
|
91
|
+
request: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
type: inspectAkanContextRequestTypeProperty,
|
|
95
|
+
app: stringProperty,
|
|
96
|
+
module: stringProperty,
|
|
97
|
+
field: stringProperty,
|
|
98
|
+
fieldType: stringProperty,
|
|
99
|
+
runIdOrPlan: stringProperty,
|
|
100
|
+
reason: stringProperty,
|
|
101
|
+
nextStep: stringProperty,
|
|
102
|
+
},
|
|
103
|
+
required: ["type"],
|
|
104
|
+
oneOf: inspectAkanContextRequestBranches,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: ["question", "draft", "review", "request"],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
111
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
112
|
+
|
|
113
|
+
const slugPart = (value: unknown) =>
|
|
114
|
+
typeof value === "string"
|
|
115
|
+
? value
|
|
116
|
+
.trim()
|
|
117
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
118
|
+
.replace(/^-+|-+$/g, "")
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
: "";
|
|
121
|
+
|
|
122
|
+
const optionalString = (args: Record<string, unknown>, key: string) => {
|
|
123
|
+
const value = args[key];
|
|
124
|
+
return typeof value === "string" && value ? value : undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const nestedStringArg = (args: Record<string, unknown>, key: string) => {
|
|
128
|
+
const value = args[key];
|
|
129
|
+
if (typeof value !== "string" || !value) throw new Error(`MCP tool argument "${key}" is required.`);
|
|
130
|
+
return value;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const isInspectAkanContextRequestType = (value: unknown): value is InspectAkanContextRequest["type"] =>
|
|
134
|
+
typeof value === "string" && inspectAkanContextRequestTypes.includes(value as InspectAkanContextRequest["type"]);
|
|
135
|
+
|
|
136
|
+
export const parseJsonOutput = (output: string) => JSON.parse(output) as unknown;
|
|
137
|
+
|
|
138
|
+
export const workspacePath = (workspace: Workspace, filePath: string) =>
|
|
139
|
+
path.isAbsolute(filePath) ? filePath : path.join(workspace.workspaceRoot, filePath);
|
|
140
|
+
|
|
141
|
+
export const defaultWorkflowPlanPath = (workflow: string, inputs: WorkflowPlanInputs) => {
|
|
142
|
+
const slug = [
|
|
143
|
+
slugPart(workflow),
|
|
144
|
+
slugPart(inputs.app),
|
|
145
|
+
slugPart(inputs.module),
|
|
146
|
+
slugPart(inputs.field),
|
|
147
|
+
slugPart(inputs.scalar),
|
|
148
|
+
slugPart(inputs.surface),
|
|
149
|
+
slugPart(inputs.mutation),
|
|
150
|
+
slugPart(inputs.slice),
|
|
151
|
+
]
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join("-");
|
|
154
|
+
return `.akan/workflows/plans/${slug || "workflow-plan"}.json`;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const applyFirstPolicy = {
|
|
158
|
+
mode: "apply-first",
|
|
159
|
+
directSourceEdits: "fallback-only",
|
|
160
|
+
applyRequiredWhen: ["plan_workflow returns planPath", "plan_workflow returns next.tool=apply_workflow"],
|
|
161
|
+
validationRequiredAfterApply: ["validationTarget", "applyReportPath"],
|
|
162
|
+
fallbackAllowedWhen: [
|
|
163
|
+
"list_workflows and explain_workflow show no matching workflow",
|
|
164
|
+
"apply_workflow reports unsupported/no-op/failed diagnostics that require manual action",
|
|
165
|
+
"recommendations include manual-action follow-up after workflow apply and repairs",
|
|
166
|
+
],
|
|
167
|
+
baselineDiagnosticsPolicy: "Do not fix unrelated baselineDiagnostics unless the user asks.",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const stringArg = (args: Record<string, unknown>, key: string) => {
|
|
171
|
+
const value = args[key];
|
|
172
|
+
if (typeof value !== "string" || !value) throw new Error(`MCP tool argument "${key}" is required.`);
|
|
173
|
+
return value;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const workflowInputsArg = (args: Record<string, unknown>) => {
|
|
177
|
+
const value = args.inputs;
|
|
178
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
179
|
+
return value as WorkflowPlanInputs;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const inspectAkanContextPropsArg = (args: Record<string, unknown>): InspectAkanContextProps => {
|
|
183
|
+
const question = stringArg(args, "question");
|
|
184
|
+
const review = stringArg(args, "review");
|
|
185
|
+
if (!isRecord(args.draft)) throw new Error('MCP tool argument "draft" is required.');
|
|
186
|
+
if (!isRecord(args.request)) throw new Error('MCP tool argument "request" is required.');
|
|
187
|
+
if (!isInspectAkanContextRequestType(args.draft.type)) {
|
|
188
|
+
throw new Error('MCP tool argument "draft.type" must be a supported inspect_akan_context request type.');
|
|
189
|
+
}
|
|
190
|
+
if (!isInspectAkanContextRequestType(args.request.type)) {
|
|
191
|
+
throw new Error('MCP tool argument "request.type" must be a supported inspect_akan_context request type.');
|
|
192
|
+
}
|
|
193
|
+
const draft = { reason: nestedStringArg(args.draft, "reason"), type: args.draft.type };
|
|
194
|
+
const request = args.request;
|
|
195
|
+
if (request.type === "workspaceOverview") return { question, draft, review, request: { type: request.type } };
|
|
196
|
+
if (request.type === "moduleContext")
|
|
197
|
+
return {
|
|
198
|
+
question,
|
|
199
|
+
draft,
|
|
200
|
+
review,
|
|
201
|
+
request: {
|
|
202
|
+
type: request.type,
|
|
203
|
+
app: nestedStringArg(request, "app"),
|
|
204
|
+
module: nestedStringArg(request, "module"),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
if (request.type === "fieldInsertionContext")
|
|
208
|
+
return {
|
|
209
|
+
question,
|
|
210
|
+
draft,
|
|
211
|
+
review,
|
|
212
|
+
request: {
|
|
213
|
+
type: request.type,
|
|
214
|
+
app: nestedStringArg(request, "app"),
|
|
215
|
+
module: nestedStringArg(request, "module"),
|
|
216
|
+
field: nestedStringArg(request, "field"),
|
|
217
|
+
fieldType: nestedStringArg(request, "fieldType"),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
if (request.type === "workflowDiagnostics")
|
|
221
|
+
return {
|
|
222
|
+
question,
|
|
223
|
+
draft,
|
|
224
|
+
review,
|
|
225
|
+
request: { type: request.type, runIdOrPlan: nestedStringArg(request, "runIdOrPlan") },
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
question,
|
|
229
|
+
draft,
|
|
230
|
+
review,
|
|
231
|
+
request: {
|
|
232
|
+
type: "escape",
|
|
233
|
+
reason: nestedStringArg(request, "reason"),
|
|
234
|
+
nextStep: optionalString(request, "nextStep"),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const inspectDiagnostic = (
|
|
240
|
+
severity: WorkflowDiagnostic["severity"],
|
|
241
|
+
code: string,
|
|
242
|
+
message: string,
|
|
243
|
+
): WorkflowDiagnostic => ({ severity, code, message });
|
|
244
|
+
|
|
245
|
+
export const moduleEvidence = (module: AkanModuleContext): AkanContextEvidence => ({
|
|
246
|
+
kind: "module",
|
|
247
|
+
target: `${module.sysName}:${module.name}`,
|
|
248
|
+
path: module.path,
|
|
249
|
+
summary: `${module.kind} module at ${module.path} with ${module.files.length} source file(s).`,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
export const moduleCandidate = (module: AkanModuleContext) => ({
|
|
253
|
+
app: module.sysName,
|
|
254
|
+
sysType: module.sysType,
|
|
255
|
+
module: module.name,
|
|
256
|
+
kind: module.kind,
|
|
257
|
+
path: module.path,
|
|
258
|
+
files: module.files,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export const expectedFieldInsertionFiles = (module: AkanModuleContext) => {
|
|
262
|
+
const sourceFiles = [`${module.name}.constant.ts`, `${module.name}.dictionary.ts`];
|
|
263
|
+
const uiFiles = module.files.filter((file) => file.endsWith(".tsx"));
|
|
264
|
+
return [...sourceFiles, ...uiFiles].map((file) => ({
|
|
265
|
+
path: `${module.path}/${file}`,
|
|
266
|
+
present: module.files.includes(file),
|
|
267
|
+
}));
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export const readonlyMcpTools: McpToolDefinition[] = [
|
|
271
|
+
{
|
|
272
|
+
name: "inspect_akan_context",
|
|
273
|
+
description:
|
|
274
|
+
"Read-only typed context inspection. Use question -> draft -> review -> request before source body reads; choose fieldInsertionContext for add-field evidence and escape when source body or outside evidence is required.",
|
|
275
|
+
inputSchema: inspectAkanContextInputSchema,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "get_workspace_summary",
|
|
279
|
+
description:
|
|
280
|
+
"Legacy broad workspace summary. Prefer inspect_akan_context.workspaceOverview for typed source-body-free evidence.",
|
|
281
|
+
inputSchema: emptySchema,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "list_apps",
|
|
285
|
+
description: "List Akan apps from the workspace context without reading source bodies.",
|
|
286
|
+
inputSchema: emptySchema,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "list_modules",
|
|
290
|
+
description: "List Akan modules across apps and libs; use inspect_akan_context.moduleContext for typed evidence.",
|
|
291
|
+
inputSchema: emptySchema,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "get_module_context",
|
|
295
|
+
description:
|
|
296
|
+
"Legacy broad module context. Pass app in monorepos; prefer inspect_akan_context.moduleContext or fieldInsertionContext for typed add-field evidence.",
|
|
297
|
+
inputSchema: { type: "object", properties: { app: stringProperty, module: stringProperty }, required: ["module"] },
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "get_guideline",
|
|
301
|
+
description: "Return an Akan agent guideline resource by name.",
|
|
302
|
+
inputSchema: { type: "object", properties: { name: stringProperty }, required: ["name"] },
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "explain_command",
|
|
306
|
+
description: "Explain one Akan CLI command and its agent-facing workflow policy.",
|
|
307
|
+
inputSchema: { type: "object", properties: { command: stringProperty }, required: ["command"] },
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "doctor_workspace",
|
|
311
|
+
description: "Report workspace diagnostics, optionally split into baseline and workflow scopes.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: { strict: booleanProperty, runIdOrPlan: stringProperty, changedFiles: stringArrayProperty },
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "get_validation_contract",
|
|
319
|
+
description: "Return validation, artifact chain, and apply-first fallback policy.",
|
|
320
|
+
inputSchema: emptySchema,
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
export const planMcpTools: McpToolDefinition[] = [
|
|
325
|
+
{
|
|
326
|
+
name: "list_workflows",
|
|
327
|
+
description: "List available read-only workflow specs before creating a plan.",
|
|
328
|
+
inputSchema: emptySchema,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "explain_workflow",
|
|
332
|
+
description: "Explain one workflow's inputs, predicted changes, validation, and completion criteria.",
|
|
333
|
+
inputSchema: { type: "object", properties: { workflow: stringProperty }, required: ["workflow"] },
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "plan_workflow",
|
|
337
|
+
description:
|
|
338
|
+
"Create a read-only workflow plan after context inspection and return planPath plus next.tool=apply_workflow.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: { workflow: stringProperty, inputs: objectProperty, out: stringProperty },
|
|
342
|
+
required: ["workflow"],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
export const applyMcpTools: McpToolDefinition[] = [
|
|
348
|
+
{
|
|
349
|
+
name: "apply_workflow",
|
|
350
|
+
description: "Apply a stored workflow plan before direct edits and return validationTarget for run_validation.",
|
|
351
|
+
inputSchema: {
|
|
352
|
+
type: "object",
|
|
353
|
+
properties: { planPath: stringProperty, dryRun: booleanProperty },
|
|
354
|
+
required: ["planPath"],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "run_validation",
|
|
359
|
+
description: "Validate a plan, apply report, validationTarget, or run artifact.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: { runIdOrPlan: stringProperty },
|
|
363
|
+
required: ["runIdOrPlan"],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "repair_generated",
|
|
368
|
+
description: "Refresh generated Akan files before direct generated-file edits.",
|
|
369
|
+
inputSchema: { type: "object", properties: { app: stringProperty }, required: ["app"] },
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "repair_imports",
|
|
373
|
+
description: "Run the import repair path before manual import edits.",
|
|
374
|
+
inputSchema: { type: "object", properties: { target: stringProperty }, required: ["target"] },
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "repair_module_shape",
|
|
378
|
+
description: "Report module-shape repair actions before direct module file fixes.",
|
|
379
|
+
inputSchema: {
|
|
380
|
+
type: "object",
|
|
381
|
+
properties: { app: stringProperty, module: stringProperty },
|
|
382
|
+
required: ["app", "module"],
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
export const listAkanMcpTools = (mode: "readonly" | "plan" | "apply" = "readonly") => {
|
|
388
|
+
if (mode === "readonly") return readonlyMcpTools;
|
|
389
|
+
if (mode === "plan") return [...readonlyMcpTools, ...planMcpTools];
|
|
390
|
+
return [...readonlyMcpTools, ...planMcpTools, ...applyMcpTools];
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
|
|
394
|
+
schemaVersion: 1,
|
|
395
|
+
reports: ["WorkflowPlan", "WorkflowApplyReport", "WorkflowValidationRunReport", "RepairReport"],
|
|
396
|
+
modes: {
|
|
397
|
+
readonly: listTools("readonly").map((tool) => tool.name),
|
|
398
|
+
plan: listTools("plan").map((tool) => tool.name),
|
|
399
|
+
apply: listTools("apply").map((tool) => tool.name),
|
|
400
|
+
},
|
|
401
|
+
validationCommands: [
|
|
402
|
+
"akan workflow validate <run-id-or-plan> --format json",
|
|
403
|
+
"akan workflow report <run-id> --format json",
|
|
404
|
+
"akan doctor --strict --format json",
|
|
405
|
+
],
|
|
406
|
+
validationFailureScopes: ["workspace-config", "environment", "source-change", "unknown"],
|
|
407
|
+
artifactChainFields: ["planPath", "applyReportPath", "repairReportPath", "runId", "validationTarget", "next"],
|
|
408
|
+
diagnosticScopes: ["baseline", "workflow", "unknown"],
|
|
409
|
+
validationStatuses: {
|
|
410
|
+
sourceStatus: ["passed", "failed", "unknown"],
|
|
411
|
+
workspaceStatus: ["passed", "failed", "unknown"],
|
|
412
|
+
overallStatus: ["passed", "failed", "blocked-by-workspace-config", "blocked-by-environment"],
|
|
413
|
+
knownBlockers: "Repeated workspace-config/environment failures are summarized before command output.",
|
|
414
|
+
},
|
|
415
|
+
moduleContextInputs: {
|
|
416
|
+
module: "required",
|
|
417
|
+
app: "required",
|
|
418
|
+
},
|
|
419
|
+
generatedFreshnessStatuses: ["fresh", "stale", "missing", "unknown"],
|
|
420
|
+
toolingRolloutGate,
|
|
421
|
+
directEditFallbackPolicy: applyFirstPolicy,
|
|
422
|
+
applyReportFields: [
|
|
423
|
+
"appliedCommands",
|
|
424
|
+
"recommendedValidationCommands",
|
|
425
|
+
"commands",
|
|
426
|
+
"recommendations",
|
|
427
|
+
"validationTarget",
|
|
428
|
+
],
|
|
429
|
+
repairCommands: [
|
|
430
|
+
"akan repair generated --app <app-or-lib> --format json",
|
|
431
|
+
"akan repair format --target <app-or-lib-or-pkg> --format json",
|
|
432
|
+
"akan repair imports --target <app-or-lib-or-pkg> --format json",
|
|
433
|
+
"akan repair dictionary --app <app-or-lib> --module <module> --format json",
|
|
434
|
+
"akan repair module-shape --app <app-or-lib> --module <module> --format json",
|
|
435
|
+
],
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
export const inspectAkanContext = async (
|
|
439
|
+
workspace: Workspace,
|
|
440
|
+
args: Record<string, unknown>,
|
|
441
|
+
): Promise<InspectAkanContextResult> => {
|
|
442
|
+
const props = inspectAkanContextPropsArg(args);
|
|
443
|
+
const diagnostics: WorkflowDiagnostic[] = [];
|
|
444
|
+
if (props.draft.type !== props.request.type) {
|
|
445
|
+
diagnostics.push(
|
|
446
|
+
inspectDiagnostic(
|
|
447
|
+
"warning",
|
|
448
|
+
"inspect-draft-request-mismatch",
|
|
449
|
+
`Draft chose ${props.draft.type}, but request uses ${props.request.type}.`,
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const baseResult = (
|
|
455
|
+
type: InspectAkanContextRequest["type"],
|
|
456
|
+
evidence: AkanContextEvidence[],
|
|
457
|
+
next: AkanContextNext,
|
|
458
|
+
data?: Record<string, unknown>,
|
|
459
|
+
): InspectAkanContextResult => ({
|
|
460
|
+
schemaVersion: 1,
|
|
461
|
+
type,
|
|
462
|
+
question: props.question,
|
|
463
|
+
diagnostics,
|
|
464
|
+
evidence,
|
|
465
|
+
next,
|
|
466
|
+
...(data ? { data } : {}),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (props.request.type === "escape") {
|
|
470
|
+
return baseResult(
|
|
471
|
+
"escape",
|
|
472
|
+
[{ kind: "escape", summary: props.request.reason }],
|
|
473
|
+
{
|
|
474
|
+
action: "escape",
|
|
475
|
+
reason: props.request.reason,
|
|
476
|
+
...(props.request.nextStep ? { args: { nextStep: props.request.nextStep } } : {}),
|
|
477
|
+
},
|
|
478
|
+
{ reason: props.request.reason, nextStep: props.request.nextStep ?? null },
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (props.request.type === "workflowDiagnostics") {
|
|
483
|
+
const doctor = await AkanContextAnalyzer.doctor(workspace, {
|
|
484
|
+
strict: true,
|
|
485
|
+
runIdOrPlan: workspacePath(workspace, props.request.runIdOrPlan),
|
|
486
|
+
});
|
|
487
|
+
diagnostics.push(
|
|
488
|
+
...doctor.diagnostics.map(
|
|
489
|
+
(diagnostic): WorkflowDiagnostic => ({
|
|
490
|
+
severity: diagnostic.severity,
|
|
491
|
+
code: diagnostic.code,
|
|
492
|
+
message: diagnostic.message,
|
|
493
|
+
scope: diagnostic.scope,
|
|
494
|
+
context: {
|
|
495
|
+
...diagnostic.context,
|
|
496
|
+
...(diagnostic.path ? { paths: [diagnostic.path] } : {}),
|
|
497
|
+
},
|
|
498
|
+
}),
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
return baseResult(
|
|
502
|
+
"workflowDiagnostics",
|
|
503
|
+
[
|
|
504
|
+
{
|
|
505
|
+
kind: "workflow",
|
|
506
|
+
target: props.request.runIdOrPlan,
|
|
507
|
+
summary: `Workflow diagnostics status is ${doctor.status}.`,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
{
|
|
511
|
+
action: doctor.status === "passed" ? "answer" : "validate",
|
|
512
|
+
reason:
|
|
513
|
+
doctor.status === "passed"
|
|
514
|
+
? "No strict workspace diagnostics were found for the workflow context."
|
|
515
|
+
: "Review diagnostics before applying or manually editing source files.",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
status: doctor.status,
|
|
519
|
+
baselineDiagnostics: doctor.baselineDiagnostics?.length ?? 0,
|
|
520
|
+
workflowDiagnostics: doctor.workflowDiagnostics?.length ?? 0,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const context = await AkanContextAnalyzer.analyze(workspace, {
|
|
526
|
+
app:
|
|
527
|
+
props.request.type === "moduleContext" || props.request.type === "fieldInsertionContext"
|
|
528
|
+
? props.request.app
|
|
529
|
+
: null,
|
|
530
|
+
module:
|
|
531
|
+
props.request.type === "moduleContext" || props.request.type === "fieldInsertionContext"
|
|
532
|
+
? props.request.module
|
|
533
|
+
: null,
|
|
534
|
+
includeAbstractContent: false,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
if (props.request.type === "workspaceOverview") {
|
|
538
|
+
return baseResult(
|
|
539
|
+
"workspaceOverview",
|
|
540
|
+
[
|
|
541
|
+
{
|
|
542
|
+
kind: "workspace",
|
|
543
|
+
summary: `${context.repoName} has ${context.apps.length} app(s), ${context.libs.length} lib(s), and ${context.pkgs.length} package(s).`,
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
{
|
|
547
|
+
action: "inspect",
|
|
548
|
+
reason: "Choose moduleContext or fieldInsertionContext for module-scoped evidence before planning a workflow.",
|
|
549
|
+
tool: "inspect_akan_context",
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
repoName: context.repoName,
|
|
553
|
+
apps: context.apps.map((app) => ({ name: app.name, path: app.path, modules: app.modules.length })),
|
|
554
|
+
libs: context.libs.map((lib) => ({ name: lib.name, path: lib.path, modules: lib.modules.length })),
|
|
555
|
+
pkgs: context.pkgs.map((pkg) => ({ name: pkg.name, path: pkg.path, version: pkg.version ?? null })),
|
|
556
|
+
generatedFiles: context.generatedFiles,
|
|
557
|
+
validationCommands: context.validationCommands,
|
|
558
|
+
},
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const modules = AkanContextAnalyzer.findModules(context, props.request.module, {
|
|
563
|
+
app: props.request.app ?? null,
|
|
564
|
+
});
|
|
565
|
+
if (modules.length === 0) {
|
|
566
|
+
diagnostics.push(
|
|
567
|
+
inspectDiagnostic(
|
|
568
|
+
"error",
|
|
569
|
+
"inspect-module-not-found",
|
|
570
|
+
`No module matched ${props.request.app ? `${props.request.app}:` : ""}${props.request.module}.`,
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
return baseResult(
|
|
574
|
+
props.request.type,
|
|
575
|
+
[],
|
|
576
|
+
{
|
|
577
|
+
action: "escape",
|
|
578
|
+
reason:
|
|
579
|
+
"The requested module was not found in the workspace index; inspect workspace files or clarify the target.",
|
|
580
|
+
},
|
|
581
|
+
{ candidates: [] },
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (!props.request.app && modules.length > 1) {
|
|
585
|
+
diagnostics.push(
|
|
586
|
+
inspectDiagnostic(
|
|
587
|
+
"error",
|
|
588
|
+
"inspect-module-ambiguous",
|
|
589
|
+
`Multiple modules match "${props.request.module}". Re-run with app to disambiguate.`,
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
return baseResult(
|
|
593
|
+
props.request.type,
|
|
594
|
+
modules.map(moduleEvidence),
|
|
595
|
+
{
|
|
596
|
+
action: "clarify",
|
|
597
|
+
reason: "Multiple modules have the same name; choose an app before planning a workflow.",
|
|
598
|
+
},
|
|
599
|
+
{ candidates: modules.map(moduleCandidate) },
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const module = modules[0];
|
|
604
|
+
if (props.request.type === "moduleContext") {
|
|
605
|
+
return baseResult(
|
|
606
|
+
"moduleContext",
|
|
607
|
+
[moduleEvidence(module)],
|
|
608
|
+
{
|
|
609
|
+
action: "inspect",
|
|
610
|
+
reason: "Use fieldInsertionContext when preparing add-field or escape when source body details are required.",
|
|
611
|
+
tool: "inspect_akan_context",
|
|
612
|
+
},
|
|
613
|
+
{ module: moduleCandidate(module) },
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (module.kind !== "domain") {
|
|
618
|
+
diagnostics.push(
|
|
619
|
+
inspectDiagnostic(
|
|
620
|
+
"error",
|
|
621
|
+
"field-insertion-unsupported-module-kind",
|
|
622
|
+
`fieldInsertionContext currently targets domain modules; ${module.sysName}:${module.name} is ${module.kind}.`,
|
|
623
|
+
),
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const moduleIndex = await buildAkanModuleContextIndex(workspace, module, { field: props.request.field });
|
|
627
|
+
diagnostics.push(...moduleIndex.diagnostics);
|
|
628
|
+
return baseResult(
|
|
629
|
+
"fieldInsertionContext",
|
|
630
|
+
[
|
|
631
|
+
moduleEvidence(module),
|
|
632
|
+
{
|
|
633
|
+
kind: "field-insertion",
|
|
634
|
+
target: `${module.sysName}:${module.name}.${props.request.field}`,
|
|
635
|
+
path: module.path,
|
|
636
|
+
summary:
|
|
637
|
+
"M2 returns source-body-free AST index evidence for constant fields, dictionary model labels, and light projection fields.",
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
module.kind === "domain"
|
|
641
|
+
? {
|
|
642
|
+
action: "plan_workflow",
|
|
643
|
+
reason: "The module target is unambiguous; create an add-field workflow plan before source edits.",
|
|
644
|
+
tool: "plan_workflow",
|
|
645
|
+
args: {
|
|
646
|
+
workflow: "add-field",
|
|
647
|
+
inputs: {
|
|
648
|
+
app: module.sysName,
|
|
649
|
+
module: module.name,
|
|
650
|
+
field: props.request.field,
|
|
651
|
+
type: props.request.fieldType,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
}
|
|
655
|
+
: {
|
|
656
|
+
action: "escape",
|
|
657
|
+
reason: "This module kind is not a supported add-field target in the P1 context contract.",
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
target: {
|
|
661
|
+
app: module.sysName,
|
|
662
|
+
module: module.name,
|
|
663
|
+
field: props.request.field,
|
|
664
|
+
fieldType: props.request.fieldType,
|
|
665
|
+
},
|
|
666
|
+
files: moduleIndex.files,
|
|
667
|
+
moduleIndex,
|
|
668
|
+
},
|
|
669
|
+
);
|
|
670
|
+
};
|