@akanjs/devkit 3.0.0-alpha.5 → 3.0.0-alpha.51

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 (55) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/agentsIndex.test.ts +10 -0
  4. package/agentsIndex.ts +47 -1
  5. package/aiEditor.ts +1 -1
  6. package/akanConfig/akanConfig.test.ts +47 -10
  7. package/akanConfig/akanConfig.ts +23 -16
  8. package/akanContext.ts +53 -11
  9. package/applicationBuildRunner.test.ts +1 -1
  10. package/applicationBuildRunner.ts +0 -1
  11. package/biome.base.json +325 -0
  12. package/biomeBase.ts +9 -0
  13. package/capacitorApp.test.ts +8 -0
  14. package/capacitorApp.ts +112 -18
  15. package/executors.test.ts +29 -5
  16. package/executors.ts +23 -5
  17. package/formSetterScanner.test.ts +80 -0
  18. package/formSetterScanner.ts +92 -0
  19. package/frontendBuild/buildRouteClient.test.ts +28 -2
  20. package/frontendBuild/clientBuildTypes.ts +4 -0
  21. package/frontendBuild/clientEntriesBundler.ts +4 -1
  22. package/frontendBuild/cssCandidateCache.test.ts +38 -0
  23. package/frontendBuild/cssCandidateCache.ts +15 -3
  24. package/frontendBuild/cssCompiler.ts +122 -11
  25. package/frontendBuild/cssImportResolver.ts +8 -7
  26. package/frontendBuild/frontendBuild.test.ts +88 -1
  27. package/frontendBuild/hmrWatcher.ts +1 -1
  28. package/frontendBuild/routeClientBuilder.ts +12 -5
  29. package/frontendBuild/ssrBaseArtifactBuilder.ts +20 -4
  30. package/frontendBuild/styleGuard.test.ts +15 -0
  31. package/frontendBuild/styleGuard.ts +17 -0
  32. package/frontendBuild/vendorSpecifiers.ts +1 -0
  33. package/getCredentials.ts +1 -3
  34. package/incrementalBuilder/devWatchBatch.test.ts +18 -20
  35. package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
  36. package/incrementalBuilder/incrementalBuilder.proc.ts +2 -2
  37. package/integration/devStabilityHarness.ts +1 -5
  38. package/lint/no-async-component-in-ui.grit +35 -0
  39. package/lint/no-daisyui-legacy-class.grit +26 -9
  40. package/lint/no-import-client-in-server.grit +48 -0
  41. package/lint/no-import-server-in-client.grit +45 -0
  42. package/lint/no-model-type-in-util-zone.grit +58 -0
  43. package/lint/no-return-in-store-action.grit +35 -0
  44. package/lint/no-unpublished-form-setter.grit +41 -0
  45. package/linter.ts +17 -12
  46. package/package.json +4 -4
  47. package/qualityScanner.test.ts +52 -0
  48. package/qualityScanner.ts +48 -37
  49. package/repoIdentity.ts +42 -0
  50. package/routeSourceValidator.test.ts +41 -0
  51. package/routeSourceValidator.ts +2 -44
  52. package/scanInfo.ts +21 -20
  53. package/transforms/externalizeFrameworkPlugin.ts +0 -1
  54. package/workspaceLayout.test.ts +56 -4
  55. package/workspaceLayout.ts +49 -4
package/README.ko.md CHANGED
@@ -56,7 +56,7 @@ await runner.build();
56
56
 
57
57
  ## 요구사항
58
58
 
59
- - [Bun](https://bun.sh) `>=1.3.13`
59
+ - [Bun](https://bun.sh) `>=1.4.0`
60
60
  - TypeScript
61
61
  - Optional peer는 Capacitor integration처럼 해당 기능을 사용할 때만 필요합니다.
62
62
 
package/README.md CHANGED
@@ -60,7 +60,7 @@ defaults, the shares they derive from `AKAN_MEMORY_LIMIT`, and what a small cont
60
60
 
61
61
  ## Requirements
62
62
 
63
- - [Bun](https://bun.sh) `>=1.3.13`
63
+ - [Bun](https://bun.sh) `>=1.4.0`
64
64
  - TypeScript
65
65
  - Optional peers are only needed for the features that use them, such as Capacitor integration.
66
66
 
@@ -10,6 +10,7 @@ import {
10
10
  renderRecipeEntries,
11
11
  renderScopeAgentBlock,
12
12
  renderScopeAgentsMd,
13
+ renderScopeClaudeMd,
13
14
  upsertAgentBlock,
14
15
  } from "./agentsIndex";
15
16
  import type { RecipeInfo } from "./recipeScanner";
@@ -48,6 +49,15 @@ describe("renderScopeAgentBlock", () => {
48
49
  });
49
50
  });
50
51
 
52
+ describe("renderScopeClaudeMd", () => {
53
+ test("points at AGENTS.md and restates only the comment rule", () => {
54
+ const claude = renderScopeClaudeMd({ type: "app", name: "minimal" });
55
+ expect(claude).toContain("@AGENTS.md");
56
+ expect(claude).toContain("## Comments — Overrides Your Default");
57
+ expect(claude).not.toContain("## Recipes In Scope");
58
+ });
59
+ });
60
+
51
61
  describe("upsertAgentBlock / extractAgentBlock", () => {
52
62
  test("round-trips: fresh file → replace block → extract equals block", () => {
53
63
  const fresh = renderScopeAgentsMd({ type: "app", name: "minimal" }, "OLD");
package/agentsIndex.ts CHANGED
@@ -19,6 +19,32 @@ const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner")
19
19
  export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
20
20
  export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
21
21
 
22
+ const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
23
+
24
+ /**
25
+ * The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
26
+ * without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
27
+ * `akan doctor` compares this against the installed devkit and says so.
28
+ */
29
+ export const stampBlockVersion = (block: string, version: string): string =>
30
+ `${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
31
+
32
+ /** The stamped version of a committed AGENTS.md, or null when it predates stamping or carries no block. */
33
+ export const extractBlockVersion = (content: string): string | null =>
34
+ content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
35
+
36
+ /** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
37
+ export const readDevkitVersion = async (): Promise<string | null> => {
38
+ const { readFile } = await import("node:fs/promises");
39
+ const { getDirname } = await import("./getDirname");
40
+ try {
41
+ const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
42
+ return (JSON.parse(raw) as { version?: string }).version ?? null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+
22
48
  export interface AgentsIndexScope {
23
49
  type: "app" | "lib";
24
50
  name: string;
@@ -139,8 +165,28 @@ ${block}
139
165
  ${AGENT_BLOCK_END}
140
166
  `;
141
167
 
142
- /** Claude Code reads nested CLAUDE.md files as it works under a directory; keep it a thin pointer. */
168
+ export const CLAUDE_COMMENT_RULE = `## Comments — Overrides Your Default
169
+
170
+ Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
171
+ is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
172
+
173
+ Before typing \`//\`, \`/*\`, or a doc block, ask — **does this sentence carry a fact that is nowhere in the code?**
174
+
175
+ - Restates the identifier, the signature, or the line under it → delete it.
176
+ - Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) → delete it.
177
+ - JSDoc on an ordinary function, or a why/how preamble on ordinary logic → delete it.
178
+ - Explains the edit you just made, for whoever reads the diff → say it in your reply, not in the file.
179
+ - Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
180
+ derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected →
181
+ keep it, one line.
182
+
183
+ That keep-list is exact — \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
184
+ subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
185
+ surrounding code carries none, your diff carries none.`;
186
+
143
187
  export const renderScopeClaudeMd = (scope: AgentsIndexScope): string => `# ${scope.name} — Claude Code Guide
144
188
 
145
189
  @AGENTS.md
190
+
191
+ ${CLAUDE_COMMENT_RULE}
146
192
  `;
package/aiEditor.ts CHANGED
@@ -19,7 +19,7 @@ import type { FileContent } from "./types";
19
19
 
20
20
  const MAX_ASK_TRY = 300;
21
21
 
22
- const deepSeekLlmModels = ["deepseek-chat", "deepseek-reasoner"] as const;
22
+ const deepSeekLlmModels = ["deepseek-v4-flash", "deepseek-v4-pro"] as const;
23
23
 
24
24
  const openAiLlmModels = ["gpt-5.5"] as const;
25
25
 
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { AppExecutor, WorkspaceExecutor } from "../executors";
6
7
  import type { PackageJson } from "../types";
7
8
  import { AkanAppConfig, AkanLibConfig, deriveDefaultAppId } from "./akanConfig";
8
9
  import type { DeepPartial, LibConfigResult } from "./types";
@@ -19,7 +20,6 @@ const packageJson: PackageJson = {
19
20
  react: "19.0.0",
20
21
  "react-dom": "19.0.0",
21
22
  "react-server-dom-webpack": "19.0.0",
22
- sharp: "1.0.0",
23
23
  "@external/runtime": "2.0.0",
24
24
  },
25
25
  };
@@ -144,8 +144,6 @@ describe("AkanAppConfig", () => {
144
144
  react: "19.0.0",
145
145
  "react-dom": "19.0.0",
146
146
  "react-server-dom-webpack": "19.0.0",
147
- croner: akanPackageJson.peerDependencies?.croner,
148
- sharp: "1.0.0",
149
147
  "@external/runtime": "2.0.0",
150
148
  },
151
149
  });
@@ -184,8 +182,6 @@ describe("AkanAppConfig", () => {
184
182
  react: runtimeDependencies.react,
185
183
  "react-dom": runtimeDependencies["react-dom"],
186
184
  "react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
187
- croner: runtimeDependencies.croner,
188
- sharp: runtimeDependencies.sharp,
189
185
  });
190
186
  });
191
187
 
@@ -198,9 +194,6 @@ describe("AkanAppConfig", () => {
198
194
  const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
199
195
  const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
200
196
 
201
- expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
202
- croner: runtimeDependencies.croner,
203
- });
204
197
  expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
205
198
  expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
206
199
  expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
@@ -210,7 +203,6 @@ describe("AkanAppConfig", () => {
210
203
  expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
211
204
  "@libsql/client": runtimeDependencies["@libsql/client"],
212
205
  bullmq: runtimeDependencies.bullmq,
213
- croner: runtimeDependencies.croner,
214
206
  ioredis: runtimeDependencies.ioredis,
215
207
  protobufjs: runtimeDependencies.protobufjs,
216
208
  });
@@ -218,7 +210,6 @@ describe("AkanAppConfig", () => {
218
210
 
219
211
  expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
220
212
  bullmq: runtimeDependencies.bullmq,
221
- croner: runtimeDependencies.croner,
222
213
  ioredis: runtimeDependencies.ioredis,
223
214
  postgres: runtimeDependencies.postgres,
224
215
  protobufjs: runtimeDependencies.protobufjs,
@@ -392,6 +383,52 @@ describe("deriveDefaultAppId", () => {
392
383
  });
393
384
  });
394
385
 
386
+ describe("AkanAppConfig lib externalLibs", () => {
387
+ test("merges lib-declared external libs into the app's own, deduped", () => {
388
+ const libAwarePackageJson: PackageJson = {
389
+ ...packageJson,
390
+ dependencies: { ...packageJson.dependencies, puppeteer: "24.0.0" },
391
+ };
392
+ const config = new AkanAppConfig(
393
+ app,
394
+ ["shared"],
395
+ libAwarePackageJson,
396
+ { externalLibs: ["@external/runtime"] },
397
+ baseDevEnv,
398
+ [],
399
+ ["@external/runtime", "puppeteer"],
400
+ );
401
+
402
+ expect(config.externalLibs).toEqual(["@external/runtime", "puppeteer"]);
403
+ expect(config.getProductionPackageJson().dependencies).toMatchObject({
404
+ "@external/runtime": "2.0.0",
405
+ puppeteer: "24.0.0",
406
+ });
407
+ });
408
+
409
+ test("reads them off every workspace lib config on load", async () => {
410
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
411
+ try {
412
+ fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
413
+ fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
414
+ fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
415
+ fs.writeFileSync(
416
+ path.join(root, "libs/extlib/akan.config.ts"),
417
+ "export default { externalLibs: ['puppeteer'] };\n",
418
+ );
419
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
420
+ fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
421
+
422
+ const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
423
+ const config = await AppExecutor.from(workspace, "extapp").getConfig();
424
+
425
+ expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
426
+ } finally {
427
+ fs.rmSync(root, { recursive: true, force: true });
428
+ }
429
+ });
430
+ });
431
+
395
432
  describe("AkanLibConfig", () => {
396
433
  test("uses empty external libs by default and preserves explicit libs", () => {
397
434
  const lib = { name: "shared" } as never;
@@ -5,7 +5,7 @@ import type { AkanPlugin } from "akanjs";
5
5
  import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
6
6
  import type { AkanImageConfig } from "akanjs/server";
7
7
  import type { App, Lib } from "../commandDecorators";
8
- import { WorkspaceExecutor } from "../executors";
8
+ import { LibExecutor, WorkspaceExecutor } from "../executors";
9
9
  import type { BaseDevEnv, PackageJson } from "../types";
10
10
  import {
11
11
  type AkanMobileConfig,
@@ -50,8 +50,6 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
50
50
  ];
51
51
  const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
52
52
  const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
53
- const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
54
- const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
55
53
  // The firebase client (push tokens) and the Capacitor toolchain — `@capacitor/cli` (`npx cap`),
56
54
  // `@capacitor/assets` (`npx @capacitor/assets`) plus the `@capacitor/core`/`ios`/`android` runtime
57
55
  // and native-platform packages that `npx cap add`/`sync` resolve from the workspace node_modules.
@@ -92,8 +90,6 @@ const DATABASE_MODE_RUNTIME_PACKAGES = {
92
90
  } satisfies Record<DatabaseMode, readonly string[]>;
93
91
  const AKAN_RUNTIME_PACKAGES = new Set<string>([
94
92
  ...SSR_RUNTIME_PACKAGES,
95
- ...NATIVE_RUNTIME_PACKAGES,
96
- ...DEFAULT_BACKEND_RUNTIME_PACKAGES,
97
93
  ...MOBILE_RUNTIME_PACKAGES,
98
94
  ...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
99
95
  ]);
@@ -199,6 +195,7 @@ export class AkanAppConfig implements AppConfigResult {
199
195
  config: DeepPartial<AppConfigResult>,
200
196
  baseDevEnv: BaseDevEnv,
201
197
  plugins: AkanPlugin[] = [],
198
+ libExternalLibs: readonly string[] = [],
202
199
  ) {
203
200
  this.app = app;
204
201
  this.rootPackageJson = rootPackageJson;
@@ -207,7 +204,7 @@ export class AkanAppConfig implements AppConfigResult {
207
204
  this.plugins = plugins;
208
205
  this.#applyRoutes(config?.routes);
209
206
  this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
210
- this.externalLibs = config?.externalLibs ?? [];
207
+ this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libExternalLibs])];
211
208
  this.barrelImports = [
212
209
  ...DEFAULT_BARREL_IMPORTS,
213
210
  ...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
@@ -416,7 +413,23 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
416
413
  ]);
417
414
  const resolved = typeof configImp === "function" ? configImp(app) : configImp;
418
415
  const { plugins, ...config } = (resolved ?? {}) as DeepPartial<AppConfigResult> & { plugins?: AkanPlugin[] };
419
- return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? []);
416
+ const libExternalLibs = await AkanAppConfig.#collectLibExternalLibs(app, libs, bustImportCache);
417
+ return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libExternalLibs);
418
+ }
419
+ //* Every workspace lib is read, not just this app's lib deps: narrowing the set needs the dependency
420
+ //* scan, and the incremental page rebundle re-reads this config on every file change.
421
+ static async #collectLibExternalLibs(app: App, libs: string[], bustImportCache: boolean): Promise<string[]> {
422
+ const libConfigs = await Promise.all(
423
+ libs.map(async (libName) =>
424
+ LibExecutor.from(app, libName)
425
+ .getConfig({ refresh: bustImportCache })
426
+ .catch((error: unknown) => {
427
+ app.logger.warn(`Skipped libs/${libName}/akan.config.ts externalLibs: ${String(error)}`);
428
+ return null;
429
+ }),
430
+ ),
431
+ );
432
+ return libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []);
420
433
  }
421
434
  #resolveProductionDependencyVersion(lib: string) {
422
435
  const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
@@ -428,13 +441,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
428
441
  return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
429
442
  }
430
443
  #getProductionRuntimePackages() {
431
- return [
432
- ...this.externalLibs,
433
- ...SSR_RUNTIME_PACKAGES,
434
- ...NATIVE_RUNTIME_PACKAGES,
435
- ...DEFAULT_BACKEND_RUNTIME_PACKAGES,
436
- ...this.getDatabaseModeRuntimePackages(),
437
- ];
444
+ return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
438
445
  }
439
446
  getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
440
447
  return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
@@ -539,8 +546,8 @@ export class AkanLibConfig implements LibConfigResult {
539
546
  this.externalLibs = config?.externalLibs ?? [];
540
547
  this.plugins = plugins;
541
548
  }
542
- static async from(lib: Lib) {
543
- const [configImp] = await Promise.all([import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default)]);
549
+ static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
550
+ const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
544
551
  const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
545
552
  const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
546
553
  return new AkanLibConfig(lib, config, plugins ?? []);
package/akanContext.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { capitalize } from "akanjs/common";
4
+ import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
4
5
  import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
5
6
  import { FileSys } from "./fileSys";
6
7
  import { collectRecipeSources, findInlineRecipeDuplicates, scanRecipes } from "./recipeScanner";
@@ -14,7 +15,7 @@ import {
14
15
  workflowRunArtifactPath,
15
16
  workflowSyncDir,
16
17
  } from "./workflow";
17
- import { appRootAllowedDirs, appRootAllowedFiles, isScannedAppRootEntry } from "./workspaceLayout";
18
+ import { isScannedRootEntry, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
18
19
 
19
20
  export type AkanContextFormat = "json" | "markdown";
20
21
  export type AkanModuleKind = "domain" | "service" | "scalar";
@@ -276,6 +277,8 @@ const validationCommands = [
276
277
  "akan test <app-or-lib-or-pkg>",
277
278
  "akan build <app-name>",
278
279
  "akan doctor --strict --format json",
280
+ "akan quality scan [--format json]",
281
+ "akan quality ssr [--format json]",
279
282
  ];
280
283
 
281
284
  const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
@@ -618,6 +621,30 @@ export class AkanContextAnalyzer {
618
621
  };
619
622
  }
620
623
 
624
+ // The conventions in AGENTS.md ship with the package, but nothing re-renders them on `bun update` — a workspace
625
+ // keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
626
+ // running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
627
+ static async #agentGuideDrift(workspace: WorkspaceExecutor) {
628
+ const installed = await readDevkitVersion();
629
+ if (!installed) return null;
630
+ const content = await safeReadText(path.join(workspace.workspaceRoot, "AGENTS.md"));
631
+ if (content === null) return null;
632
+ const stamped = extractBlockVersion(content);
633
+ if (stamped === installed) return null;
634
+ const hint = "Re-render the AGENTS.md managed block from the installed framework release.";
635
+ if (!stamped)
636
+ return {
637
+ code: "agent-guide-unstamped",
638
+ message: `AGENTS.md carries no generated-version stamp, so its conventions may predate @akanjs/devkit ${installed}`,
639
+ hint,
640
+ };
641
+ return {
642
+ code: "agent-guide-stale",
643
+ message: `AGENTS.md was generated by @akanjs/devkit ${stamped}, but ${installed} is installed`,
644
+ hint,
645
+ };
646
+ }
647
+
621
648
  static async doctor(
622
649
  workspace: WorkspaceExecutor,
623
650
  {
@@ -639,23 +666,25 @@ export class AkanContextAnalyzer {
639
666
  ),
640
667
  ];
641
668
 
642
- for (const app of context.apps) {
643
- const appPath = path.join(workspace.workspaceRoot, app.path);
644
- for (const entry of await safeReadDir(appPath)) {
645
- if (!isScannedAppRootEntry(entry.name)) continue;
646
- const allowed = entry.isDirectory() ? appRootAllowedDirs.has(entry.name) : appRootAllowedFiles.has(entry.name);
669
+ for (const sys of [...context.apps, ...context.libs]) {
670
+ const sysPath = path.join(workspace.workspaceRoot, sys.path);
671
+ for (const entry of await safeReadDir(sysPath)) {
672
+ if (!isScannedRootEntry(sys.type, entry.name)) continue;
673
+ const allowed = entry.isDirectory()
674
+ ? rootAllowedDirs[sys.type].has(entry.name)
675
+ : rootAllowedFiles[sys.type].has(entry.name);
647
676
  if (!allowed) {
648
677
  const action = repairAction(
649
678
  "module-shape",
650
- `akan repair module-shape --app ${app.name}`,
651
- "Review app root shape and remove or move the unknown entry.",
679
+ `akan repair module-shape --app ${sys.name}`,
680
+ `Review ${sys.type} root shape and remove or move the unknown entry.`,
652
681
  false,
653
682
  );
654
683
  diagnostics.push({
655
684
  severity: "error",
656
- code: "app-root-unknown-entry",
657
- path: `${app.path}/${entry.name}`,
658
- message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in app root: ${app.path}/${entry.name}`,
685
+ code: `${sys.type}-root-unknown-entry`,
686
+ path: `${sys.path}/${entry.name}`,
687
+ message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
659
688
  repairActions: [action],
660
689
  });
661
690
  repairActions.push(action);
@@ -663,6 +692,19 @@ export class AkanContextAnalyzer {
663
692
  }
664
693
  }
665
694
 
695
+ const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
696
+ if (agentDrift) {
697
+ const action = repairAction("generated", "akan agent install agents-md", agentDrift.hint, true);
698
+ diagnostics.push({
699
+ severity: "warning",
700
+ code: agentDrift.code,
701
+ path: "AGENTS.md",
702
+ message: agentDrift.message,
703
+ repairActions: [action],
704
+ });
705
+ repairActions.push(action);
706
+ }
707
+
666
708
  for (const sys of [...context.apps, ...context.libs]) {
667
709
  for (const module of sys.modules) {
668
710
  if (!module.abstract.exists) {
@@ -4,7 +4,7 @@ import { AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
4
4
  describe("ApplicationBuildRunner", () => {
5
5
  test("externalizes Akan optional backend dependencies", () => {
6
6
  expect(AKAN_OPTIONAL_BACKEND_EXTERNALS).toEqual(
7
- expect.arrayContaining(["@libsql/client", "bullmq", "croner", "ioredis", "postgres", "protobufjs"]),
7
+ expect.arrayContaining(["@libsql/client", "bullmq", "ioredis", "postgres", "protobufjs"]),
8
8
  );
9
9
  });
10
10
  });
@@ -56,7 +56,6 @@ const SSR_RENDER_EXTERNALS = [
56
56
  export const AKAN_OPTIONAL_BACKEND_EXTERNALS = [
57
57
  "@libsql/client",
58
58
  "bullmq",
59
- "croner",
60
59
  "ioredis",
61
60
  "postgres",
62
61
  "protobufjs",