@akanjs/devkit 3.1.1-rc.16 → 3.1.1-rc.18
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/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanContext.ts +44 -2
- package/applicationBuildRunner.ts +1 -0
- package/codeAgent/agent/akanSystemPrompt.ts +1 -1
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/skills/akan-agent/SKILL.md +36 -0
- package/codeAgent/skills/akan-debug/SKILL.md +3 -3
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +38 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/akanCodePlugins.test.ts +12 -30
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/executors.test.ts +0 -4
- package/maintainabilityReport.ts +11 -0
- package/package.json +2 -2
- package/prompter.ts +7 -0
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/workflow/artifacts.ts +10 -1
- package/workflow/render.ts +4 -3
- package/workflow/source.test.ts +55 -0
- package/workflow/source.ts +29 -10
- package/workflow/types.ts +1 -1
|
@@ -24,6 +24,12 @@ try {
|
|
|
24
24
|
const configPath = process.env.AKAN_TYPECHECK_TSCONFIG;
|
|
25
25
|
if (!configPath) throw new Error("AKAN_TYPECHECK_TSCONFIG is required");
|
|
26
26
|
const result = TypeChecker.checkProject(configPath);
|
|
27
|
+
if (process.env.AKAN_TYPECHECK_FORMAT === "json") {
|
|
28
|
+
//? Not process.cwd(): that is the realpath (/private/var on macOS) while TypeScript keeps the spelling it was given.
|
|
29
|
+
const root = process.env.AKAN_TYPECHECK_ROOT ?? process.cwd();
|
|
30
|
+
process.stdout.write(JSON.stringify(TypeChecker.toRecords(result.errors, root)));
|
|
31
|
+
process.exit(result.errors.length > 0 ? 1 : 0);
|
|
32
|
+
}
|
|
27
33
|
if (result.errors.length > 0) {
|
|
28
34
|
console.error(result.message);
|
|
29
35
|
process.exit(1);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TypecheckRecord } from "../typeChecker";
|
|
2
|
+
|
|
3
|
+
export class TypecheckFailure extends Error {
|
|
4
|
+
constructor(readonly records: TypecheckRecord[]) {
|
|
5
|
+
super(
|
|
6
|
+
records
|
|
7
|
+
.slice(0, 20)
|
|
8
|
+
.map((record) => `${record.file ?? "-"}:${record.line}:${record.col} ${record.code} ${record.message}`)
|
|
9
|
+
.join("\n") || "Typecheck failed",
|
|
10
|
+
);
|
|
11
|
+
this.name = "TypecheckFailure";
|
|
12
|
+
}
|
|
13
|
+
}
|
package/workflow/artifacts.ts
CHANGED
|
@@ -282,6 +282,15 @@ const statusForValidationKind = (
|
|
|
282
282
|
return matching.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
+
//? `akan verify` syncs first and names a failed sync in its header line (`sync 1 error`), so one run answers both.
|
|
286
|
+
const generatedSyncStatus = (commands: readonly WorkflowValidationCommandResult[]): WorkflowValidationStatus => {
|
|
287
|
+
const sync = statusForValidationKind(commands, "sync");
|
|
288
|
+
const verify = commands.filter((command) => command.kind === "verify");
|
|
289
|
+
if (sync !== "unknown" || verify.length === 0) return sync;
|
|
290
|
+
const syncFailed = verify.some((command) => /^verify fail · .*\bsync \d+ errors?\b/m.test(command.stdout ?? ""));
|
|
291
|
+
return syncFailed ? "failed" : "passed";
|
|
292
|
+
};
|
|
293
|
+
|
|
285
294
|
const statusForCommands = (commands: readonly WorkflowValidationCommandResult[]): WorkflowValidationStatus => {
|
|
286
295
|
if (commands.length === 0) return "unknown";
|
|
287
296
|
return commandStatus(commands) === "failed" ? "failed" : "passed";
|
|
@@ -438,7 +447,7 @@ const createValidationStatuses = (
|
|
|
438
447
|
overallStatus,
|
|
439
448
|
summary: {
|
|
440
449
|
sourceChange: sourceStatus,
|
|
441
|
-
generatedSync:
|
|
450
|
+
generatedSync: generatedSyncStatus(commands),
|
|
442
451
|
validationCommands: validationCommandsStatus,
|
|
443
452
|
baseline: baselineStatus,
|
|
444
453
|
workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
|
package/workflow/render.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { camelToKebabCase } from "../commandDecorators/camelToKebabCase";
|
|
1
2
|
import type {
|
|
2
3
|
PrimitiveFormat,
|
|
3
4
|
PrimitiveWriteReport,
|
|
@@ -63,9 +64,9 @@ export const renderWorkflowExplain = (spec: WorkflowSpec) =>
|
|
|
63
64
|
"## Inputs",
|
|
64
65
|
...Object.entries(spec.inputs).map(
|
|
65
66
|
([name, input]) =>
|
|
66
|
-
`- \`${name}\`${
|
|
67
|
-
input.
|
|
68
|
-
}`,
|
|
67
|
+
`- \`${name}\`${camelToKebabCase(name) === name ? "" : ` (\`--${camelToKebabCase(name)}\`)`}${
|
|
68
|
+
input.required ? " (required)" : ""
|
|
69
|
+
}: ${input.description}${input.allowedValues ? ` Allowed: ${input.allowedValues.join(", ")}.` : ""}`,
|
|
69
70
|
),
|
|
70
71
|
"",
|
|
71
72
|
"## Optional Surfaces",
|
package/workflow/source.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
hasSourceParseErrors,
|
|
11
11
|
insertClassMethod,
|
|
12
12
|
insertDictionaryEndpointEntry,
|
|
13
|
+
insertDictionaryEnum,
|
|
13
14
|
insertSignalFactoryEntry,
|
|
14
15
|
inspectDictionaryStructure,
|
|
15
16
|
} from "./source";
|
|
@@ -251,6 +252,60 @@ describe("dictionary endpoint entries", () => {
|
|
|
251
252
|
});
|
|
252
253
|
});
|
|
253
254
|
|
|
255
|
+
describe("insertDictionaryEnum", () => {
|
|
256
|
+
const dictionary = (tail: string) => `import { modelDictionary } from "akanjs/dictionary";
|
|
257
|
+
|
|
258
|
+
import type { Task } from "./task.constant";
|
|
259
|
+
|
|
260
|
+
// Stages: .model() field labels, .enum() value labels, .error() messages, .translate() UI messages.
|
|
261
|
+
|
|
262
|
+
export const dictionary = modelDictionary(["en", "ko"])
|
|
263
|
+
.of((t) => t(["Task", "할 일"]))
|
|
264
|
+
.model<Task>((t) => ({
|
|
265
|
+
title: t(["Title", "제목"]),
|
|
266
|
+
}))${tail};
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
test("inserts before the chain's own .error() stage, not a comment that names it", () => {
|
|
270
|
+
const next = insertDictionaryEnum(
|
|
271
|
+
dictionary(`
|
|
272
|
+
.error({ notFound: ["Not found", "없음"] })
|
|
273
|
+
.translate({})`),
|
|
274
|
+
"TaskPriority",
|
|
275
|
+
"taskPriority",
|
|
276
|
+
["low", "high"],
|
|
277
|
+
);
|
|
278
|
+
expect(next).not.toBeNull();
|
|
279
|
+
expect(hasSourceParseErrors(next ?? "", "task.dictionary.ts")).toBe(false);
|
|
280
|
+
expect(next?.split("\n").slice(-7)).toEqual([
|
|
281
|
+
' .enum<TaskPriority>("taskPriority", (t) => ({',
|
|
282
|
+
' low: t(["Low", "Low"]),',
|
|
283
|
+
' high: t(["High", "High"]),',
|
|
284
|
+
" }))",
|
|
285
|
+
' .error({ notFound: ["Not found", "없음"] })',
|
|
286
|
+
" .translate({});",
|
|
287
|
+
"",
|
|
288
|
+
]);
|
|
289
|
+
expect(inspectDictionaryStructure(next ?? "", "Task").chainOrderValid).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("appends to the end of a chain with no later stage", () => {
|
|
293
|
+
const next = insertDictionaryEnum(dictionary(""), "TaskPriority", "taskPriority", ["low"]);
|
|
294
|
+
expect(hasSourceParseErrors(next ?? "", "task.dictionary.ts")).toBe(false);
|
|
295
|
+
expect(next).toContain(
|
|
296
|
+
' }))\n .enum<TaskPriority>("taskPriority", (t) => ({\n low: t(["Low", "Low"]),\n }));\n',
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("leaves an existing enum stage alone and refuses a file that does not parse", () => {
|
|
301
|
+
const withEnum = insertDictionaryEnum(dictionary(""), "TaskPriority", "taskPriority", ["low"]) ?? "";
|
|
302
|
+
expect(insertDictionaryEnum(withEnum, "TaskPriority", "taskPriority", ["low"])).toBe(withEnum);
|
|
303
|
+
expect(
|
|
304
|
+
insertDictionaryEnum("export const dictionary = modelDictionary([", "TaskPriority", "taskPriority", ["low"]),
|
|
305
|
+
).toBeNull();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
254
309
|
describe("classMethodParameters", () => {
|
|
255
310
|
test("reads names and which parameters a caller may leave out", () => {
|
|
256
311
|
const content = `export class NoteService {
|
package/workflow/source.ts
CHANGED
|
@@ -58,14 +58,12 @@ export const generatedFilesForSync = (sys: Sys, reason = "Generated files may ch
|
|
|
58
58
|
|
|
59
59
|
export const validationCommandsForTarget = (target: string) =>
|
|
60
60
|
[
|
|
61
|
-
{ command: `akan
|
|
62
|
-
{ command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
|
|
61
|
+
{ command: `akan verify ${target}`, reason: "Sync, lint, typecheck and build the target in one run." },
|
|
63
62
|
] satisfies PrimitiveValidationCommand[];
|
|
64
63
|
|
|
65
64
|
export const nextActionsForTarget = (target: string) =>
|
|
66
65
|
[
|
|
67
|
-
{ command: `akan
|
|
68
|
-
{ command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
|
|
66
|
+
{ command: `akan verify ${target}`, reason: "Refresh generated files and validate the target in one run." },
|
|
69
67
|
] satisfies PrimitiveNextAction[];
|
|
70
68
|
|
|
71
69
|
export const createPrimitiveWriteReport = ({
|
|
@@ -898,17 +896,38 @@ export const ensureConstantTypeImport = (content: string, constantPath: string,
|
|
|
898
896
|
return `import type { ${typeName} } from "${constantPath}";\n${content}`;
|
|
899
897
|
};
|
|
900
898
|
|
|
899
|
+
const dictionaryChainOf = (node: ts.Node): ts.CallExpression | undefined => {
|
|
900
|
+
if (
|
|
901
|
+
ts.isCallExpression(node) &&
|
|
902
|
+
outermostFluentCall(node) === node &&
|
|
903
|
+
protectedDictionaryChainOrder.some((method) => chainMethodsForCall(node).includes(method))
|
|
904
|
+
)
|
|
905
|
+
return node;
|
|
906
|
+
return ts.forEachChild(node, dictionaryChainOf);
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const chainMethodNames = (call: ts.Expression): ts.Identifier[] => {
|
|
910
|
+
if (!ts.isCallExpression(call) || !ts.isPropertyAccessExpression(call.expression)) return [];
|
|
911
|
+
const name = call.expression.name;
|
|
912
|
+
return [...chainMethodNames(call.expression.expression), ...(ts.isIdentifier(name) ? [name] : [])];
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
//* Located on the parsed chain, never by text search: the sample dictionary's header comment mentions `.error()`.
|
|
901
916
|
export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
902
917
|
if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
|
|
918
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
919
|
+
if (hasParseDiagnostics(source)) return null;
|
|
920
|
+
const chain = dictionaryChainOf(source);
|
|
921
|
+
if (!chain) return null;
|
|
903
922
|
const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
|
|
904
923
|
.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
|
|
905
924
|
.join("\n")}\n }))\n`;
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (
|
|
911
|
-
return
|
|
925
|
+
const later = chainMethodNames(chain).find((name) => name.text === "error" || name.text === "translate");
|
|
926
|
+
if (!later) return spliceText(content, chain.getEnd(), chain.getEnd(), `\n${enumBlock.trimEnd()}`);
|
|
927
|
+
const dot = content.lastIndexOf(".", later.getStart(source));
|
|
928
|
+
const lineStart = lineStartAt(content, dot);
|
|
929
|
+
if (content.slice(lineStart, dot).trim() === "") return spliceText(content, lineStart, lineStart, enumBlock);
|
|
930
|
+
return spliceText(content, dot, dot, `\n${enumBlock}`);
|
|
912
931
|
};
|
|
913
932
|
|
|
914
933
|
export const parseValues = (value: string | null) =>
|
package/workflow/types.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type WorkflowFormat = "markdown" | "json";
|
|
|
8
8
|
export type WorkflowPlanInputs = Record<string, string | null>;
|
|
9
9
|
export type PrimitiveFormat = "markdown" | "json";
|
|
10
10
|
export type UiSurface = "view" | "unit" | "template";
|
|
11
|
-
export type WorkflowValidationKind = "sync" | "lint" | "typecheck" | "doctor" | "custom";
|
|
11
|
+
export type WorkflowValidationKind = "verify" | "sync" | "lint" | "typecheck" | "doctor" | "custom";
|
|
12
12
|
export type WorkflowFailureScope = "workspace-config" | "environment" | "source-change" | "unknown";
|
|
13
13
|
export type WorkflowValidationStatus = "passed" | "failed" | "unknown";
|
|
14
14
|
export interface WorkflowValidationSummary {
|