@akanjs/devkit 2.3.9-rc.3 → 2.3.9-rc.4

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.
@@ -0,0 +1,224 @@
1
+ import type { Sys } from "../commandDecorators";
2
+ import { generatedFilePathsForTarget } from "./artifacts";
3
+ import { createPrimitiveWriteReport } from "./primitive";
4
+ import type {
5
+ PrimitiveChangedFile,
6
+ PrimitiveFileMap,
7
+ PrimitiveGeneratedFile,
8
+ PrimitiveNextAction,
9
+ PrimitiveValidationCommand,
10
+ WorkflowDiagnostic,
11
+ } from "./types";
12
+
13
+ export const getSysRoot = (sys: Sys) => `${sys.type}s/${sys.name}`;
14
+
15
+ export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile["action"], reason: string) => ({
16
+ path: `${getSysRoot(sys)}/${path}`,
17
+ action,
18
+ reason,
19
+ });
20
+
21
+ export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
22
+ generatedFilePathsForTarget(getSysRoot(sys), reason);
23
+
24
+ export const validationCommandsForTarget = (target: string) =>
25
+ [
26
+ { command: `akan sync ${target}`, reason: "Refresh generated Akan files from source conventions." },
27
+ { command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
28
+ ] satisfies PrimitiveValidationCommand[];
29
+
30
+ export const nextActionsForTarget = (target: string) =>
31
+ [
32
+ { command: `akan sync ${target}`, reason: "Refresh generated Akan files after source changes." },
33
+ { command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
34
+ ] satisfies PrimitiveNextAction[];
35
+
36
+ export const createPassedPrimitiveReport = ({
37
+ command,
38
+ changedFiles,
39
+ generatedFiles,
40
+ target,
41
+ nextActions,
42
+ }: {
43
+ command: string;
44
+ changedFiles: PrimitiveChangedFile[];
45
+ generatedFiles?: PrimitiveGeneratedFile[];
46
+ target: string;
47
+ nextActions?: PrimitiveNextAction[];
48
+ }) =>
49
+ createPrimitiveWriteReport({
50
+ command,
51
+ changedFiles,
52
+ generatedFiles: generatedFiles ?? [],
53
+ validationCommands: validationCommandsForTarget(target),
54
+ diagnostics: [],
55
+ nextActions: nextActions ?? nextActionsForTarget(target),
56
+ });
57
+
58
+ export const scalarChangedFiles = (sys: Sys, scalarName: string, files: PrimitiveFileMap) =>
59
+ Object.values(files).map((file) =>
60
+ sourceFile(sys, `lib/__scalar/${scalarName}/${file.filename}`, "create", "Scalar source file was created."),
61
+ );
62
+
63
+ export const titleize = (value: string) =>
64
+ value
65
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
66
+ .replace(/[-_]+/g, " ")
67
+ .replace(/\b\w/g, (char) => char.toUpperCase());
68
+
69
+ export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
70
+
71
+ export const normalizeFieldType = (typeName: string) => {
72
+ const normalizedTypes: Record<string, string> = {
73
+ string: "String",
74
+ boolean: "Boolean",
75
+ date: "Date",
76
+ int: "Int",
77
+ integer: "Int",
78
+ float: "Float",
79
+ double: "Float",
80
+ decimal: "Float",
81
+ };
82
+ return normalizedTypes[typeName.toLowerCase()] ?? typeName;
83
+ };
84
+
85
+ export const ensureBaseTypeImport = (content: string, typeName: string) => {
86
+ if (typeName !== "Int" && typeName !== "Float") return content;
87
+ if (new RegExp(`import \\{[^}]*\\b${typeName}\\b[^}]*\\} from "akanjs/base";`).test(content)) return content;
88
+ const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
89
+ if (baseImport) {
90
+ const names = baseImport[1]
91
+ .split(",")
92
+ .map((name) => name.trim())
93
+ .filter(Boolean);
94
+ return content.replace(baseImport[0], `import { ${[...names, typeName].sort().join(", ")} } from "akanjs/base";`);
95
+ }
96
+ return `import { ${typeName} } from "akanjs/base";\n${content}`;
97
+ };
98
+
99
+ const numericDefault = (typeName: "Int" | "Float", rawDefault: string): string | null => {
100
+ const trimmed = rawDefault.trim();
101
+ if (!trimmed || !Number.isFinite(Number(trimmed))) return null;
102
+ if (typeName === "Int" && !/^-?\d+$/.test(trimmed)) return null;
103
+ return trimmed;
104
+ };
105
+
106
+ export const coerceFieldDefault = (
107
+ typeName: string,
108
+ defaultValue?: string | null,
109
+ ): { expression: string | null; diagnostic?: WorkflowDiagnostic } => {
110
+ if (defaultValue === undefined || defaultValue === null || defaultValue === "") return { expression: null };
111
+ const normalizedType = normalizeFieldType(typeName);
112
+ if (normalizedType === "Int" || normalizedType === "Float") {
113
+ const expression = numericDefault(normalizedType, defaultValue);
114
+ if (expression !== null) return { expression };
115
+ return {
116
+ expression: null,
117
+ diagnostic: {
118
+ severity: "error",
119
+ code: "primitive-default-value-invalid",
120
+ input: "default",
121
+ failureScope: "source-change",
122
+ message: `Default value for ${normalizedType} must be a numeric literal. Received: ${JSON.stringify(defaultValue)}.`,
123
+ },
124
+ };
125
+ }
126
+ if (normalizedType === "Boolean") {
127
+ const lowered = defaultValue.trim().toLowerCase();
128
+ if (lowered === "true" || lowered === "false") return { expression: lowered };
129
+ return {
130
+ expression: null,
131
+ diagnostic: {
132
+ severity: "error",
133
+ code: "primitive-default-value-invalid",
134
+ input: "default",
135
+ failureScope: "source-change",
136
+ message: `Default value for Boolean must be true or false. Received: ${JSON.stringify(defaultValue)}.`,
137
+ },
138
+ };
139
+ }
140
+ return { expression: JSON.stringify(defaultValue) };
141
+ };
142
+
143
+ export const fieldExpression = (typeName: string, defaultValue?: string | null) => {
144
+ const typeExpression = normalizeFieldType(typeName);
145
+ const defaultExpression = coerceFieldDefault(typeExpression, defaultValue).expression;
146
+ const defaultOption = defaultExpression ? `, { default: ${defaultExpression} }` : "";
147
+ return `field(${typeExpression}${defaultOption})`;
148
+ };
149
+
150
+ export const insertIntoObject = (content: string, className: string, line: string) => {
151
+ const classIndex = content.indexOf(`export class ${className} extends via`);
152
+ if (classIndex < 0) return null;
153
+ const objectEndIndex = content.indexOf("}))", classIndex);
154
+ if (objectEndIndex < 0) return null;
155
+ const prefix = content.slice(0, objectEndIndex);
156
+ const suffix = content.slice(objectEndIndex);
157
+ const insertion = prefix.endsWith("\n") ? ` ${line}\n` : `\n ${line}\n`;
158
+ return `${prefix}${insertion}${suffix}`;
159
+ };
160
+
161
+ export const ensureEnumImport = (content: string) => {
162
+ if (content.includes("enumOf")) return content;
163
+ const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
164
+ if (baseImport) {
165
+ const names = baseImport[1]?.split(",").map((name) => name.trim()) ?? [];
166
+ return content.replace(baseImport[0], `import { ${[...names, "enumOf"].sort().join(", ")} } from "akanjs/base";`);
167
+ }
168
+ return `import { enumOf } from "akanjs/base";\n${content}`;
169
+ };
170
+
171
+ export const insertEnumClass = (content: string, enumClassName: string, enumName: string, values: string[]) => {
172
+ if (content.includes(`export class ${enumClassName} extends enumOf`)) return content;
173
+ const enumClass = `export class ${enumClassName} extends enumOf("${enumName}", [\n${values
174
+ .map((value) => ` ${JSON.stringify(value)},`)
175
+ .join("\n")}\n] as const) {}\n\n`;
176
+ const firstClassIndex = content.indexOf("export class ");
177
+ if (firstClassIndex < 0) return `${content}\n${enumClass}`;
178
+ return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
179
+ };
180
+
181
+ export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
182
+ if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
183
+ const label = titleize(fieldName);
184
+ const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
185
+ if (modelIndex < 0) return null;
186
+ const objectEndIndex = content.indexOf(" }))", modelIndex);
187
+ if (objectEndIndex < 0) return null;
188
+ return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label)}, ${JSON.stringify(
189
+ label,
190
+ )}]).desc([${JSON.stringify(label)}, ${JSON.stringify(label)}]),\n${content.slice(objectEndIndex)}`;
191
+ };
192
+
193
+ export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
194
+ if (new RegExp(`import type \\{[^}]*\\b${typeName}\\b[^}]*\\} from "${constantPath}";`).test(content)) return content;
195
+ const importPattern = new RegExp(`import type \\{ ([^}]+) \\} from "${constantPath}";`);
196
+ const existingImport = content.match(importPattern);
197
+ if (existingImport !== null) {
198
+ const names = existingImport[1]?.split(",").map((name) => name.trim()) ?? [];
199
+ return content.replace(
200
+ existingImport[0],
201
+ `import type { ${[...names, typeName].sort().join(", ")} } from "${constantPath}";`,
202
+ );
203
+ }
204
+ return `import type { ${typeName} } from "${constantPath}";\n${content}`;
205
+ };
206
+
207
+ export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
208
+ if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
209
+ const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
210
+ .map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
211
+ .join("\n")}\n }))\n`;
212
+ const chainEndIndex = content.lastIndexOf(";");
213
+ const insertBeforeIndex = [content.indexOf(".error("), content.indexOf(".translate("), chainEndIndex]
214
+ .filter((index) => index >= 0)
215
+ .sort((a, b) => a - b)[0];
216
+ if (insertBeforeIndex === undefined) return null;
217
+ return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
218
+ };
219
+
220
+ export const parseValues = (value: string | null) =>
221
+ value
222
+ ?.split(",")
223
+ .map((item) => item.trim())
224
+ .filter(Boolean) ?? [];
@@ -0,0 +1,283 @@
1
+ import type { Sys, Workspace } from "../commandDecorators";
2
+
3
+ export type WorkflowInputType = "string" | "string-list" | "surface-mode";
4
+ export type WorkflowSurfaceMode = "infer" | "include" | "skip";
5
+ export type WorkflowInputValue = string | string[] | WorkflowSurfaceMode;
6
+ export type WorkflowFormat = "markdown" | "json";
7
+ export type WorkflowPlanInputs = Record<string, string | null>;
8
+ export type PrimitiveFormat = "markdown" | "json";
9
+ export type UiSurface = "view" | "unit" | "template";
10
+ export type WorkflowValidationKind = "sync" | "lint" | "typecheck" | "doctor" | "custom";
11
+ export type WorkflowFailureScope = "workspace-config" | "environment" | "source-change" | "unknown";
12
+ export type WorkflowValidationStatus = "passed" | "failed" | "unknown";
13
+ export type WorkflowOverallStatus = "passed" | "failed" | "blocked-by-workspace-config" | "blocked-by-environment";
14
+
15
+ export interface PrimitiveTargetInput {
16
+ app: string | null;
17
+ module: string | null;
18
+ }
19
+
20
+ export interface AddFieldInput extends PrimitiveTargetInput {
21
+ field: string | null;
22
+ type: string | null;
23
+ defaultValue?: string | null;
24
+ }
25
+
26
+ export interface AddEnumFieldInput extends PrimitiveTargetInput {
27
+ field: string | null;
28
+ values: string | null;
29
+ defaultValue?: string | null;
30
+ }
31
+
32
+ export interface WorkflowInputSpec {
33
+ type: WorkflowInputType;
34
+ required?: boolean;
35
+ description: string;
36
+ allowedValues?: readonly string[];
37
+ }
38
+
39
+ export interface WorkflowStep {
40
+ id: string;
41
+ title: string;
42
+ tool: string;
43
+ description: string;
44
+ when?: string;
45
+ }
46
+
47
+ export interface WorkflowPredictedChange {
48
+ target: string;
49
+ action: "inspect" | "create" | "modify" | "sync" | "validate";
50
+ reason: string;
51
+ applyScope?: "auto" | "manual-review" | "generated-sync" | "validation";
52
+ }
53
+
54
+ export interface WorkflowValidation {
55
+ command: string;
56
+ reason: string;
57
+ kind?: WorkflowValidationKind;
58
+ }
59
+
60
+ export interface WorkflowSpec {
61
+ schemaVersion: 1;
62
+ name: string;
63
+ description: string;
64
+ whenToUse: string;
65
+ inputs: Record<string, WorkflowInputSpec>;
66
+ optionalSurfaces?: Record<string, WorkflowSurfaceMode>;
67
+ steps: readonly WorkflowStep[];
68
+ predictedChanges: readonly WorkflowPredictedChange[];
69
+ validation: readonly WorkflowValidation[];
70
+ completionCriteria: readonly string[];
71
+ }
72
+
73
+ export interface WorkflowDiagnostic {
74
+ severity: "warning" | "error";
75
+ code: string;
76
+ message: string;
77
+ input?: string;
78
+ command?: string;
79
+ kind?: WorkflowValidationKind;
80
+ failureScope?: WorkflowFailureScope;
81
+ scope?: "baseline" | "workflow" | "unknown";
82
+ context?: {
83
+ workflow?: string;
84
+ planPath?: string;
85
+ runId?: string;
86
+ target?: string;
87
+ paths?: string[];
88
+ };
89
+ }
90
+
91
+ export interface WorkflowRecommendation {
92
+ code: string;
93
+ message: string;
94
+ kind: "auto-apply" | "validation" | "manual-action" | "import" | "placement" | "ui-component";
95
+ target?: string;
96
+ action?: string;
97
+ confidence?: "high" | "medium" | "low";
98
+ }
99
+
100
+ export interface WorkflowPlan {
101
+ schemaVersion: 1;
102
+ workflow: string;
103
+ mode: "plan";
104
+ inputs: Record<string, WorkflowInputValue>;
105
+ optionalSurfaces: Record<string, WorkflowSurfaceMode>;
106
+ steps: readonly WorkflowStep[];
107
+ predictedChanges: readonly WorkflowPredictedChange[];
108
+ validation: readonly WorkflowValidation[];
109
+ diagnostics: WorkflowDiagnostic[];
110
+ recommendations: WorkflowRecommendation[];
111
+ requiresApproval: true;
112
+ }
113
+
114
+ export interface WorkflowReport {
115
+ schemaVersion: 1;
116
+ workflow: string;
117
+ mode: "plan" | "dry-run" | "apply";
118
+ status: "passed" | "failed";
119
+ diagnostics: WorkflowDiagnostic[];
120
+ plan?: WorkflowPlan;
121
+ }
122
+
123
+ export interface WorkflowApplyCommand {
124
+ command: string;
125
+ reason: string;
126
+ stepId?: string;
127
+ kind?: WorkflowValidationKind;
128
+ }
129
+
130
+ export type WorkflowRunSource =
131
+ | { type: "plan"; path: string }
132
+ | { type: "apply-report"; path: string; runId?: string }
133
+ | { type: "run-report"; runId: string };
134
+
135
+ export interface WorkflowValidationCommandResult {
136
+ command: string;
137
+ reason: string;
138
+ kind?: WorkflowValidationKind;
139
+ status: "passed" | "failed";
140
+ exitCode: number;
141
+ failureScope?: WorkflowFailureScope;
142
+ stdout?: string;
143
+ stderr?: string;
144
+ }
145
+
146
+ export interface WorkflowKnownBlocker {
147
+ code: string;
148
+ message: string;
149
+ failureScope: WorkflowFailureScope;
150
+ command?: string;
151
+ kind?: WorkflowValidationKind;
152
+ count: number;
153
+ }
154
+
155
+ export interface RepairAction {
156
+ command: string;
157
+ reason: string;
158
+ kind: "generated" | "format" | "imports" | "dictionary" | "module-shape";
159
+ safeToRun: boolean;
160
+ }
161
+
162
+ export interface PrimitiveChangedFile {
163
+ path: string;
164
+ action: "create" | "modify" | "remove";
165
+ reason: string;
166
+ }
167
+
168
+ export interface PrimitiveGeneratedFile {
169
+ path: string;
170
+ action: "sync";
171
+ reason: string;
172
+ }
173
+
174
+ export interface PrimitiveValidationCommand {
175
+ command: string;
176
+ reason: string;
177
+ }
178
+
179
+ export interface PrimitiveNextAction {
180
+ command: string;
181
+ reason: string;
182
+ }
183
+
184
+ export interface PrimitiveWriteReport {
185
+ schemaVersion: 1;
186
+ command: string;
187
+ status: "passed" | "failed";
188
+ changedFiles: PrimitiveChangedFile[];
189
+ generatedFiles: PrimitiveGeneratedFile[];
190
+ validationCommands: PrimitiveValidationCommand[];
191
+ diagnostics: WorkflowDiagnostic[];
192
+ nextActions: PrimitiveNextAction[];
193
+ }
194
+
195
+ export type PrimitiveFileMap = Record<string, { filename: string; content: string }>;
196
+
197
+ export interface WorkflowApplyReport {
198
+ schemaVersion: 1;
199
+ runId?: string;
200
+ applyReportPath?: string;
201
+ validationTarget?: string;
202
+ workflow: string;
203
+ mode: "dry-run" | "apply";
204
+ status: "passed" | "failed";
205
+ changedFiles: PrimitiveChangedFile[];
206
+ generatedFiles: PrimitiveGeneratedFile[];
207
+ appliedCommands: WorkflowApplyCommand[];
208
+ recommendedValidationCommands: WorkflowApplyCommand[];
209
+ commands: WorkflowApplyCommand[];
210
+ diagnostics: WorkflowDiagnostic[];
211
+ recommendations: WorkflowRecommendation[];
212
+ nextActions: PrimitiveNextAction[];
213
+ plan: WorkflowPlan;
214
+ }
215
+
216
+ export interface WorkflowValidationRunReport {
217
+ schemaVersion: 1;
218
+ runId: string;
219
+ workflow: string;
220
+ mode: "validate";
221
+ source: WorkflowRunSource;
222
+ status: "passed" | "failed";
223
+ sourceStatus: WorkflowValidationStatus;
224
+ workspaceStatus: WorkflowValidationStatus;
225
+ overallStatus: WorkflowOverallStatus;
226
+ knownBlockers: WorkflowKnownBlocker[];
227
+ commands: WorkflowValidationCommandResult[];
228
+ diagnostics: WorkflowDiagnostic[];
229
+ baselineDiagnostics?: WorkflowDiagnostic[];
230
+ workflowDiagnostics?: WorkflowDiagnostic[];
231
+ repairActions: RepairAction[];
232
+ nextActions: PrimitiveNextAction[];
233
+ plan?: WorkflowPlan;
234
+ }
235
+
236
+ export interface RepairReport {
237
+ schemaVersion: 1;
238
+ runId?: string;
239
+ repairReportPath?: string;
240
+ command: string;
241
+ kind: RepairAction["kind"];
242
+ target: string | null;
243
+ status: "passed" | "failed";
244
+ diagnostics: WorkflowDiagnostic[];
245
+ repairActions: RepairAction[];
246
+ nextActions: PrimitiveNextAction[];
247
+ commands: WorkflowValidationCommandResult[];
248
+ generatedFiles?: PrimitiveGeneratedFile[];
249
+ syncedAt?: string;
250
+ }
251
+
252
+ export interface GeneratedSyncState {
253
+ schemaVersion: 1;
254
+ target: string;
255
+ status: "passed" | "failed";
256
+ syncedAt: string;
257
+ command: string;
258
+ runId?: string;
259
+ generatedFiles: PrimitiveGeneratedFile[];
260
+ }
261
+
262
+ export type WorkflowRunArtifact = WorkflowApplyReport | WorkflowValidationRunReport | RepairReport;
263
+
264
+ export interface WorkflowStepResult {
265
+ changedFiles?: PrimitiveChangedFile[];
266
+ generatedFiles?: PrimitiveGeneratedFile[];
267
+ commands?: WorkflowApplyCommand[];
268
+ diagnostics?: WorkflowDiagnostic[];
269
+ recommendations?: WorkflowRecommendation[];
270
+ nextActions?: PrimitiveNextAction[];
271
+ }
272
+
273
+ export type WorkflowStepRunner = (step: WorkflowStep, plan: WorkflowPlan) => Promise<WorkflowStepResult | undefined>;
274
+ export type WorkflowStepRegistry = Record<string, WorkflowStepRunner>;
275
+
276
+ export interface WorkflowPrimitiveOperations {
277
+ workspace: Workspace;
278
+ createModule: (sys: Sys, module: string) => Promise<PrimitiveWriteReport>;
279
+ createScalar: (sys: Sys, scalar: string) => Promise<PrimitiveWriteReport>;
280
+ createUi: (input: PrimitiveTargetInput & { surface: UiSurface }) => Promise<PrimitiveWriteReport>;
281
+ addField: (input: AddFieldInput) => Promise<PrimitiveWriteReport>;
282
+ addEnumField: (input: AddEnumFieldInput) => Promise<PrimitiveWriteReport>;
283
+ }
@@ -0,0 +1,23 @@
1
+ import type { WorkflowDiagnostic, WorkflowValidationCommandResult } from "./types";
2
+
3
+ export const jsonText = (value: unknown, { trailingNewline = true }: { trailingNewline?: boolean } = {}) =>
4
+ `${JSON.stringify(value, null, 2)}${trailingNewline ? "\n" : ""}`;
5
+
6
+ export const workflowStatus = (diagnostics: readonly WorkflowDiagnostic[]) =>
7
+ diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed";
8
+
9
+ export const commandStatus = (commands: readonly WorkflowValidationCommandResult[]) =>
10
+ commands.some((command) => command.status === "failed") ? "failed" : "passed";
11
+
12
+ export const uniqueBy = <T>(values: readonly T[], key: (value: T) => string) => {
13
+ const seen = new Set<string>();
14
+ return values.filter((value) => {
15
+ const itemKey = key(value);
16
+ if (seen.has(itemKey)) return false;
17
+ seen.add(itemKey);
18
+ return true;
19
+ });
20
+ };
21
+
22
+ export const compactDiagnostics = (diagnostics: Array<WorkflowDiagnostic | false | null | undefined>) =>
23
+ diagnostics.filter((diagnostic): diagnostic is WorkflowDiagnostic => !!diagnostic);