@akanjs/devkit 2.3.9-rc.5 → 2.3.9-rc.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.3.9-rc.5",
3
+ "version": "2.3.9-rc.7",
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.5",
35
+ "akanjs": "2.3.9-rc.7",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "5.5.23",
@@ -28,6 +28,7 @@ export const createWorkflowApplyReport = ({
28
28
  recommendedValidationCommands,
29
29
  commands = [],
30
30
  diagnostics = [],
31
+ postApplyChecks = [],
31
32
  recommendations = [],
32
33
  nextActions = [],
33
34
  plan,
@@ -41,14 +42,21 @@ export const createWorkflowApplyReport = ({
41
42
  | "appliedCommands"
42
43
  | "recommendedValidationCommands"
43
44
  | "commands"
45
+ | "postApplyChecks"
44
46
  | "recommendations"
45
47
  > & {
46
48
  appliedCommands?: WorkflowApplyCommand[];
47
49
  recommendedValidationCommands?: WorkflowApplyCommand[];
48
50
  commands?: WorkflowApplyCommand[];
51
+ postApplyChecks?: WorkflowApplyReport["postApplyChecks"];
49
52
  recommendations?: WorkflowApplyReport["recommendations"];
50
53
  }): WorkflowApplyReport => {
51
54
  const validationCommands = recommendedValidationCommands ?? commands;
55
+ const orderedNextActions = uniqueBy(nextActions, (action) => action.command).sort((left, right) => {
56
+ const leftRepair = left.command.startsWith("akan workflow explain") ? 0 : 1;
57
+ const rightRepair = right.command.startsWith("akan workflow explain") ? 0 : 1;
58
+ return leftRepair - rightRepair;
59
+ });
52
60
  return {
53
61
  schemaVersion: 1,
54
62
  workflow,
@@ -60,8 +68,9 @@ export const createWorkflowApplyReport = ({
60
68
  recommendedValidationCommands: uniqueBy(validationCommands, (command) => command.command),
61
69
  commands: uniqueBy(validationCommands, (command) => command.command),
62
70
  diagnostics,
71
+ postApplyChecks,
63
72
  recommendations: uniqueBy(recommendations, (recommendation) => `${recommendation.kind}:${recommendation.code}`),
64
- nextActions: uniqueBy(nextActions, (action) => action.command),
73
+ nextActions: orderedNextActions.slice(0, 3),
65
74
  plan,
66
75
  };
67
76
  };
@@ -1,4 +1,5 @@
1
1
  import { capitalize } from "akanjs/common";
2
+ import ts from "typescript";
2
3
  import type { Sys, Workspace } from "../commandDecorators";
3
4
  import { AppExecutor, LibExecutor } from "../executors";
4
5
  import { createWorkflowApplyReport, workflowCommandsForPlan } from "./artifacts";
@@ -12,7 +13,9 @@ import type {
12
13
  WorkflowDiagnostic,
13
14
  WorkflowInputValue,
14
15
  WorkflowPlan,
16
+ WorkflowPostApplyCheck,
15
17
  WorkflowPrimitiveOperations,
18
+ WorkflowRecommendation,
16
19
  WorkflowStep,
17
20
  WorkflowStepRegistry,
18
21
  WorkflowStepResult,
@@ -36,6 +39,103 @@ const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
36
39
  const workflowStringArrayInput = (value: WorkflowInputValue | undefined) => (Array.isArray(value) ? value : null);
37
40
  const workflowBooleanInput = (value: WorkflowInputValue | undefined) => (typeof value === "boolean" ? value : null);
38
41
 
42
+ const postApplyDiagnostic = (code: string, message: string, target: string): WorkflowDiagnostic => ({
43
+ severity: "error",
44
+ code,
45
+ message,
46
+ failureScope: "source-change",
47
+ context: { target },
48
+ });
49
+
50
+ const sourceKindForPath = (filePath: string) => {
51
+ if (filePath.endsWith(".tsx")) return ts.ScriptKind.TSX;
52
+ if (filePath.endsWith(".ts")) return ts.ScriptKind.TS;
53
+ return null;
54
+ };
55
+
56
+ const checkPathCasing = async (workspace: Workspace, filePath: string) => {
57
+ const segments = filePath.split("/").filter(Boolean);
58
+ let current = ".";
59
+ for (const segment of segments) {
60
+ const entries = await workspace.readdir(current);
61
+ if (entries.includes(segment)) {
62
+ current = current === "." ? segment : `${current}/${segment}`;
63
+ continue;
64
+ }
65
+ const caseInsensitiveMatch = entries.find((entry) => entry.toLowerCase() === segment.toLowerCase());
66
+ return caseInsensitiveMatch
67
+ ? {
68
+ code: "workflow-path-casing-mismatch",
69
+ message: `Reported path segment "${segment}" does not match actual casing "${caseInsensitiveMatch}" in ${filePath}.`,
70
+ }
71
+ : {
72
+ code: "workflow-path-missing",
73
+ message: `Reported path does not exist: ${filePath}.`,
74
+ };
75
+ }
76
+ return null;
77
+ };
78
+
79
+ const checkTypeScriptSyntax = async (workspace: Workspace, filePath: string) => {
80
+ const scriptKind = sourceKindForPath(filePath);
81
+ if (!scriptKind) return null;
82
+ const content = await workspace.readFile(filePath);
83
+ const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
84
+ const diagnostic = source.parseDiagnostics[0];
85
+ if (!diagnostic) return null;
86
+ const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
87
+ const location = position ? `:${position.line + 1}:${position.character + 1}` : "";
88
+ return {
89
+ code: "workflow-post-apply-syntax-error",
90
+ message: `Generated source has a TypeScript syntax error at ${filePath}${location}: ${ts.flattenDiagnosticMessageText(
91
+ diagnostic.messageText,
92
+ " ",
93
+ )}`,
94
+ };
95
+ };
96
+
97
+ const checkChangedFile = async (workspace: Workspace, file: PrimitiveChangedFile): Promise<WorkflowStepResult> => {
98
+ if (file.action === "remove") return { postApplyChecks: [] };
99
+ const diagnostics: WorkflowDiagnostic[] = [];
100
+ const checks: WorkflowPostApplyCheck[] = [];
101
+ const pathIssue = await checkPathCasing(workspace, file.path);
102
+ if (pathIssue) {
103
+ diagnostics.push(postApplyDiagnostic(pathIssue.code, pathIssue.message, file.path));
104
+ checks.push({ ...pathIssue, target: file.path, status: "failed" });
105
+ return { diagnostics, postApplyChecks: checks };
106
+ }
107
+ const syntaxIssue = await checkTypeScriptSyntax(workspace, file.path);
108
+ if (syntaxIssue) {
109
+ diagnostics.push(postApplyDiagnostic(syntaxIssue.code, syntaxIssue.message, file.path));
110
+ checks.push({ ...syntaxIssue, target: file.path, status: "failed" });
111
+ return { diagnostics, postApplyChecks: checks };
112
+ }
113
+ checks.push({
114
+ code: "workflow-post-apply-file-valid",
115
+ target: file.path,
116
+ status: "passed",
117
+ message: "Changed file exists with exact casing and parses as source when applicable.",
118
+ });
119
+ return { postApplyChecks: checks };
120
+ };
121
+
122
+ const checkRecommendationPath = async (
123
+ workspace: Workspace,
124
+ recommendation: WorkflowRecommendation,
125
+ ): Promise<WorkflowDiagnostic | null> => {
126
+ if (!recommendation.target || recommendation.target.includes("*") || recommendation.target.startsWith("<"))
127
+ return null;
128
+ const pathIssue = await checkPathCasing(workspace, recommendation.target);
129
+ if (!pathIssue) return null;
130
+ return {
131
+ severity: "warning",
132
+ code: "workflow-recommendation-path-unverified",
133
+ message: `${pathIssue.message} Recommendation "${recommendation.code}" should not be treated as an exact file target until the path is corrected.`,
134
+ failureScope: "source-change",
135
+ context: { target: recommendation.target },
136
+ };
137
+ };
138
+
39
139
  const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
40
140
  if (!target) return null;
41
141
  const [apps, libs] = await workspace.getSyss();
@@ -197,13 +297,17 @@ export const createWorkflowStepCommandResult = (
197
297
  });
198
298
 
199
299
  export class WorkflowExecutor {
200
- constructor(private readonly registry: WorkflowStepRegistry) {}
300
+ constructor(
301
+ private readonly registry: WorkflowStepRegistry,
302
+ private readonly workspace?: Workspace,
303
+ ) {}
201
304
 
202
305
  async apply(plan: WorkflowPlan) {
203
306
  const changedFiles: PrimitiveChangedFile[] = [];
204
307
  const generatedFiles: PrimitiveGeneratedFile[] = [];
205
308
  const recommendedValidationCommands: WorkflowApplyCommand[] = [];
206
309
  const diagnostics: WorkflowDiagnostic[] = [...plan.diagnostics];
310
+ const postApplyChecks: WorkflowPostApplyCheck[] = [];
207
311
  const recommendations = [...plan.recommendations];
208
312
  const nextActions: PrimitiveNextAction[] = [];
209
313
 
@@ -215,6 +319,7 @@ export class WorkflowExecutor {
215
319
  generatedFiles,
216
320
  recommendedValidationCommands,
217
321
  diagnostics,
322
+ postApplyChecks,
218
323
  recommendations,
219
324
  nextActions,
220
325
  plan,
@@ -251,6 +356,20 @@ export class WorkflowExecutor {
251
356
  if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) break;
252
357
  }
253
358
 
359
+ if (this.workspace && !diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
360
+ for (const file of changedFiles) {
361
+ const result = await checkChangedFile(this.workspace, file);
362
+ postApplyChecks.push(...(result.postApplyChecks ?? []));
363
+ diagnostics.push(...(result.diagnostics ?? []));
364
+ }
365
+ const recommendationDiagnostics = await Promise.all(
366
+ recommendations.map((recommendation) => checkRecommendationPath(this.workspace as Workspace, recommendation)),
367
+ );
368
+ diagnostics.push(
369
+ ...recommendationDiagnostics.filter((diagnostic): diagnostic is WorkflowDiagnostic => !!diagnostic),
370
+ );
371
+ }
372
+
254
373
  return createWorkflowApplyReport({
255
374
  workflow: plan.workflow,
256
375
  mode: "apply",
@@ -258,6 +377,7 @@ export class WorkflowExecutor {
258
377
  generatedFiles,
259
378
  recommendedValidationCommands,
260
379
  diagnostics,
380
+ postApplyChecks,
261
381
  recommendations,
262
382
  nextActions,
263
383
  plan,
@@ -93,6 +93,17 @@ const renderRecommendation = (recommendation: WorkflowApplyReport["recommendatio
93
93
  const renderDiagnostic = (diagnostic: WorkflowApplyReport["diagnostics"][number]) =>
94
94
  `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`;
95
95
 
96
+ const applySourceStatus = (report: WorkflowApplyReport) =>
97
+ report.diagnostics.some(
98
+ (diagnostic) =>
99
+ diagnostic.severity === "error" &&
100
+ (diagnostic.failureScope === "source-change" ||
101
+ !diagnostic.failureScope ||
102
+ diagnostic.failureScope === "unknown"),
103
+ )
104
+ ? "failed"
105
+ : "passed";
106
+
96
107
  export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
97
108
  const manualReviewItems = [
98
109
  ...report.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map(renderDiagnostic),
@@ -107,13 +118,32 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
107
118
  (diagnostic.failureScope === "workspace-config" || diagnostic.failureScope === "environment"),
108
119
  )
109
120
  .map(renderDiagnostic);
121
+ const sourceBlockers = report.diagnostics
122
+ .filter(
123
+ (diagnostic) =>
124
+ diagnostic.severity === "error" &&
125
+ (diagnostic.failureScope === "source-change" ||
126
+ !diagnostic.failureScope ||
127
+ diagnostic.failureScope === "unknown"),
128
+ )
129
+ .map(renderDiagnostic);
110
130
  return [
111
131
  `# Workflow Apply: ${report.workflow}`,
112
132
  "",
113
133
  `- Mode: ${report.mode}`,
114
134
  `- Status: ${report.status}`,
135
+ `- Source-change status: ${applySourceStatus(report)}`,
136
+ `- Workspace status: ${validationBlockers.length ? "blocked" : "not blocked during apply"}`,
115
137
  ...(report.validationTarget ? [`- Validation target: ${report.validationTarget}`] : []),
116
138
  "",
139
+ "## Apply Checks",
140
+ ...(report.postApplyChecks?.length
141
+ ? report.postApplyChecks.map((check) => `- [${check.status}] ${check.code} ${check.target}: ${check.message}`)
142
+ : ["- not run"]),
143
+ "",
144
+ "## Source Change Blockers",
145
+ ...(sourceBlockers.length ? sourceBlockers : ["- none"]),
146
+ "",
117
147
  "## Automatically Modified",
118
148
  ...(report.changedFiles.length
119
149
  ? report.changedFiles.map((file) => `- \`${file.action}\` ${file.path}: ${file.reason}`)
@@ -148,7 +178,7 @@ export const renderWorkflowApplyReport = (report: WorkflowApplyReport) => {
148
178
  ...(report.diagnostics.length ? report.diagnostics.map(renderDiagnostic) : ["- none"]),
149
179
  "",
150
180
  "## Recommendations",
151
- ...(report.recommendations.length ? report.recommendations.map(renderRecommendation) : ["- none"]),
181
+ ...(report.recommendations.length ? report.recommendations.slice(0, 3).map(renderRecommendation) : ["- none"]),
152
182
  "",
153
183
  "## Next Actions",
154
184
  ...(report.nextActions.length
@@ -172,10 +202,24 @@ export const renderWorkflowValidationRunReport = (report: WorkflowValidationRunR
172
202
  `- Overall status: ${report.overallStatus}`,
173
203
  "",
174
204
  "## Status Summary",
175
- `- Required source-change validation: ${report.summary.sourceChange}`,
176
- `- Generated sync validation: ${report.summary.generatedSync}`,
177
- `- Workspace configuration validation: ${report.summary.workspaceConfig}`,
178
- `- Environment validation: ${report.summary.environment}`,
205
+ `- Source-change: ${report.summary.sourceChange}`,
206
+ `- Generated sync: ${report.summary.generatedSync}`,
207
+ `- Workspace config: ${report.summary.workspaceConfig}`,
208
+ `- Environment: ${report.summary.environment}`,
209
+ "",
210
+ "## Source Change Diagnostics",
211
+ ...(report.workflowDiagnostics?.length
212
+ ? report.workflowDiagnostics.map(
213
+ (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
214
+ )
215
+ : ["- none"]),
216
+ "",
217
+ "## Existing Workspace Blockers",
218
+ ...(report.baselineDiagnostics?.length
219
+ ? report.baselineDiagnostics.map(
220
+ (diagnostic) => `- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
221
+ )
222
+ : ["- none"]),
179
223
  "",
180
224
  "## Known Blockers",
181
225
  ...(report.knownBlockers.length
@@ -290,7 +290,7 @@ export const coerceFieldDefault = (
290
290
  export const fieldExpression = (
291
291
  typeName: string,
292
292
  defaultValue?: FieldDefaultValue,
293
- options: { enumValues?: readonly string[] | null } = {},
293
+ options: { enumValues?: readonly string[] | null; builderName?: string } = {},
294
294
  ) => {
295
295
  const typeExpression = normalizeFieldType(typeName);
296
296
  const defaultExpression = coerceFieldDefault(
@@ -299,7 +299,14 @@ export const fieldExpression = (
299
299
  options,
300
300
  ).expression;
301
301
  const defaultOption = defaultExpression ? `, { default: ${defaultExpression} }` : "";
302
- return `field(${typeExpression}${defaultOption})`;
302
+ return `${options.builderName ?? "field"}(${typeExpression}${defaultOption})`;
303
+ };
304
+
305
+ export const viaBuilderParameterName = (content: string, className: string) => {
306
+ const classIndex = content.indexOf(`export class ${className} extends via`);
307
+ if (classIndex < 0) return null;
308
+ const signature = content.slice(classIndex, content.indexOf("=>", classIndex));
309
+ return /via\(\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(signature)?.[1] ?? null;
303
310
  };
304
311
 
305
312
  export const insertIntoObject = (content: string, className: string, line: string) => {
@@ -379,17 +386,60 @@ export const insertEnumClass = (content: string, enumClassName: string, enumName
379
386
  return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
380
387
  };
381
388
 
382
- export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
383
- if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
389
+ const findMatchingBrace = (content: string, openIndex: number) => {
390
+ let depth = 0;
391
+ let quote: '"' | "'" | "`" | null = null;
392
+ let escaped = false;
393
+ for (let index = openIndex; index < content.length; index++) {
394
+ const char = content[index];
395
+ if (quote) {
396
+ if (escaped) {
397
+ escaped = false;
398
+ continue;
399
+ }
400
+ if (char === "\\") {
401
+ escaped = true;
402
+ continue;
403
+ }
404
+ if (char === quote) quote = null;
405
+ continue;
406
+ }
407
+ if (char === '"' || char === "'" || char === "`") {
408
+ quote = char;
409
+ continue;
410
+ }
411
+ if (char === "{") depth += 1;
412
+ if (char === "}") {
413
+ depth -= 1;
414
+ if (depth === 0) return index;
415
+ }
416
+ }
417
+ return -1;
418
+ };
419
+
420
+ const dictionaryModelFieldLine = (fieldName: string) => {
384
421
  const label = bilingualLabelForField(fieldName);
385
422
  const desc = bilingualDescriptionForField(fieldName);
386
- const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
423
+ return `${fieldName}: t([${JSON.stringify(label.en)}, ${JSON.stringify(label.ko)}]).desc([${JSON.stringify(
424
+ desc.en,
425
+ )}, ${JSON.stringify(desc.ko)}]),`;
426
+ };
427
+
428
+ export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
429
+ if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
430
+ const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => (`);
387
431
  if (modelIndex < 0) return null;
388
- const objectEndIndex = content.indexOf(" }))", modelIndex);
432
+ const objectStartIndex = content.indexOf("{", modelIndex);
433
+ if (objectStartIndex < 0) return null;
434
+ const objectEndIndex = findMatchingBrace(content, objectStartIndex);
389
435
  if (objectEndIndex < 0) return null;
390
- return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label.en)}, ${JSON.stringify(
391
- label.ko,
392
- )}]).desc([${JSON.stringify(desc.en)}, ${JSON.stringify(desc.ko)}]),\n${content.slice(objectEndIndex)}`;
436
+ const fieldLine = dictionaryModelFieldLine(fieldName);
437
+ const body = content.slice(objectStartIndex + 1, objectEndIndex);
438
+ if (body.trim().length === 0) {
439
+ return `${content.slice(0, objectStartIndex + 1)}\n ${fieldLine}\n ${content.slice(objectEndIndex)}`;
440
+ }
441
+ const insertion = body.endsWith("\n") ? ` ${fieldLine}\n` : `\n ${fieldLine}\n`;
442
+ return `${content.slice(0, objectEndIndex)}${insertion}${content.slice(objectEndIndex)}`;
393
443
  };
394
444
 
395
445
  export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
package/workflow/types.ts CHANGED
@@ -177,6 +177,13 @@ export interface PrimitiveChangedFile {
177
177
  reason: string;
178
178
  }
179
179
 
180
+ export interface WorkflowPostApplyCheck {
181
+ code: string;
182
+ target: string;
183
+ status: "passed" | "failed";
184
+ message: string;
185
+ }
186
+
180
187
  export interface PrimitiveGeneratedFile {
181
188
  path: string;
182
189
  action: "sync";
@@ -220,6 +227,7 @@ export interface WorkflowApplyReport {
220
227
  recommendedValidationCommands: WorkflowApplyCommand[];
221
228
  commands: WorkflowApplyCommand[];
222
229
  diagnostics: WorkflowDiagnostic[];
230
+ postApplyChecks?: WorkflowPostApplyCheck[];
223
231
  recommendations: WorkflowRecommendation[];
224
232
  nextActions: PrimitiveNextAction[];
225
233
  plan: WorkflowPlan;
@@ -279,6 +287,7 @@ export interface WorkflowStepResult {
279
287
  generatedFiles?: PrimitiveGeneratedFile[];
280
288
  commands?: WorkflowApplyCommand[];
281
289
  diagnostics?: WorkflowDiagnostic[];
290
+ postApplyChecks?: WorkflowPostApplyCheck[];
282
291
  recommendations?: WorkflowRecommendation[];
283
292
  nextActions?: PrimitiveNextAction[];
284
293
  }