@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.
Files changed (38) hide show
  1. package/agentAssets.test.ts +65 -0
  2. package/agentAssets.ts +166 -0
  3. package/agentsIndex.ts +4 -4
  4. package/akanContext.ts +44 -2
  5. package/applicationBuildRunner.ts +1 -0
  6. package/codeAgent/agent/akanSystemPrompt.ts +1 -1
  7. package/codeAgent/feedback/VerifyFeedback.ts +3 -4
  8. package/codeAgent/skills/akan-agent/SKILL.md +36 -0
  9. package/codeAgent/skills/akan-debug/SKILL.md +3 -3
  10. package/codeAgent/skills/akan-deps/SKILL.md +1 -2
  11. package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
  12. package/codeAgent/skills/akan-page/SKILL.md +44 -0
  13. package/codeAgent/skills/akan-query/SKILL.md +38 -0
  14. package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
  15. package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
  16. package/codeAgent/skills/akan-transport/SKILL.md +40 -0
  17. package/codeAgent/skills/akan-validate/SKILL.md +13 -11
  18. package/codeAgent/tools/AkanCli.ts +2 -1
  19. package/codeAgent/tools/AkanEditScope.ts +8 -3
  20. package/codeAgent/tools/AkanToolPack.ts +4 -10
  21. package/codeAgent/tools/AkanVerifier.ts +13 -31
  22. package/codeAgent/tools/akanCodePlugins.test.ts +12 -30
  23. package/conventionSet.test.ts +73 -0
  24. package/conventionSet.ts +67 -0
  25. package/executors.test.ts +0 -4
  26. package/maintainabilityReport.ts +11 -0
  27. package/package.json +2 -2
  28. package/prompter.ts +7 -0
  29. package/typeChecker.ts +32 -0
  30. package/typecheck/projectTypecheck.test.ts +18 -0
  31. package/typecheck/projectTypecheck.ts +29 -5
  32. package/typecheck/typecheck.proc.ts +6 -0
  33. package/typecheck/typecheckFailure.ts +13 -0
  34. package/workflow/artifacts.ts +10 -1
  35. package/workflow/render.ts +4 -3
  36. package/workflow/source.test.ts +55 -0
  37. package/workflow/source.ts +29 -10
  38. 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
+ }
@@ -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: statusForValidationKind(commands, "sync"),
450
+ generatedSync: generatedSyncStatus(commands),
442
451
  validationCommands: validationCommandsStatus,
443
452
  baseline: baselineStatus,
444
453
  workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
@@ -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}\`${input.required ? " (required)" : ""}: ${input.description}${
67
- input.allowedValues ? ` Allowed: ${input.allowedValues.join(", ")}.` : ""
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",
@@ -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 {
@@ -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 sync ${target}`, reason: "Refresh generated Akan files from source conventions." },
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 sync ${target}`, reason: "Refresh generated Akan files after source changes." },
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 chainEndIndex = content.lastIndexOf(";");
907
- const insertBeforeIndex = [content.indexOf(".error("), content.indexOf(".translate("), chainEndIndex]
908
- .filter((index) => index >= 0)
909
- .sort((a, b) => a - b)[0];
910
- if (insertBeforeIndex === undefined) return null;
911
- return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
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 {