@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.31

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 (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +2 -2
  3. package/README.md +3 -2
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +427 -141
  19. package/akanConfig/akanConfig.ts +327 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +21 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +269 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +177 -0
  241. package/mobile/desktopBin.ts +143 -0
  242. package/mobile/desktopServerStage.test.ts +204 -0
  243. package/mobile/desktopServerStage.ts +138 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +177 -0
  250. package/mobile/nativeApp.ts +416 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +414 -0
  254. package/mobile/nativeConfig.ts +293 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +30 -25
  298. package/subspace.ts +81 -115
  299. package/subspaceConfig.ts +11 -9
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -22,7 +22,7 @@ crosses its ceiling is replaced when it is next idle — never mid-work.
22
22
 
23
23
  | process | explicit (MiB) | explicit (bytes) | share of the limit | fallback with no limit |
24
24
  |---|---|---|---|---|
25
- | incremental builder | `AKAN_BUILDER_MAX_RSS_MB` | `AKAN_BUILDER_MAX_RSS` | **0.35** | 1200MB (dev) |
25
+ | incremental builder | `AKAN_BUILDER_MAX_RSS_MB` | `AKAN_BUILDER_MAX_RSS` | **0.35** | 2048MB (dev) |
26
26
  | RSC worker | `AKAN_RSC_WORKER_MAX_RSS_MB` | `AKAN_RSC_WORKER_MAX_RSS` | **0.55** | 768MB (dev), unbounded (production) |
27
27
 
28
28
  `AKAN_BUILDER_MAX_RSS_MB=0` leaves the builder unbounded, which is the escape hatch for an app whose
@@ -37,8 +37,10 @@ this repo's own `apps/akan`). None of those has a ceiling, and none is subtracte
37
37
 
38
38
  In practice this holds because the two ceilings are rarely at their limit simultaneously and the build
39
39
  worker exits. But if you are sizing a container to a hard number, size it against the *sum of observed
40
- peaks*, not against these fractions. Measured floors, for reference: builder 134-202MB at rest and
41
- ~490MB after a route build, RSC worker ~142MB after boot, ~247MB per route build.
40
+ peaks*, not against these fractions. Measured on `apps/minimal` (macOS, 2026-09-29), for reference: the
41
+ builder ~120MB at rest after boot, ~540MB right after its first route build (the bundler hands most of it
42
+ back after ~20s idle), ~250MB while it patches saves into the SSR registry; the RSC worker ~75MB after boot
43
+ and ~110MB after route builds; the build worker ~900MB for the SSR registry's boot build.
42
44
 
43
45
  ## Recycling behaviour
44
46
 
@@ -65,10 +67,17 @@ which is the one case where replacing it again cannot help; it says that too, an
65
67
 
66
68
  | variable | default | what it does |
67
69
  |---|---|---|
68
- | `AKAN_DEV_CSR_REBUILD` | off | Rebuild the CSR artifact on every save. Armed automatically by the first `/__csr` or `?csr=true` request, which is what a mobile WebView session does — set it explicitly only to have it from boot. |
70
+ | `AKAN_DEV_CSR_REBUILD` | off | Rebuild the CSR artifact on every save. Armed automatically by the first `/__csr` or `?csr=true` request, which is what a mobile WebView session does — set it explicitly only to have it from boot. Set at boot, its CSR build runs beside the SSR registry's: on apps/akan the builder and its workers peak at about 2.7GB instead of 1.3GB, and the next boot wave waits for both builds. |
71
+ | `AKAN_DEV_CSR` | `registry` | How the dev CSR page is built. `registry` serves it as a module registry that patches the modules a save changed; `artifact` brings back the single-file bundle, which reloads on every save. |
72
+ | `AKAN_DEV_CSR_PATCHER` | on | `off` sends every save of a dev module registry — the CSR page's, and the one SSR pages load their client code from (`.akan/artifact/ssr-dev`) — to a disposable build worker instead of patching it in the resident builder. The patcher keeps each registry in memory (on minimal the CSR one adds about 170MB to the builder's median RSS and 210MB to its peak, flat across saves) and hands whole-app builds to a worker: a first build, a new npm module, a bare import no file of its package resolved before, a config change, a signal or dictionary save (their metadata is inlined at build time), and a registry whose last whole build was cut short. A misspelled import fails in the builder, like a missing file. A builder that dies mid-patch turns it off until the dev server next replaces its builder and backend together (a config, signal or dictionary change). |
73
+ | `AKAN_DEV_WATCH_DEBOUNCE_MS` | `30` | How long the file watcher collects changes before it hands a batch over. Every save waits this long before anything builds. A second write inside the window joins the same batch; one after it starts another batch, which reads the file's latest content, so a format-on-save costs a second build rather than a wrong one. |
69
74
  | `AKAN_BUILDER_RPC_TIMEOUT_MS` | `120000` | How long the backend waits for a builder answer. Generous on purpose: a cold CSR build of every page legitimately takes tens of seconds. |
70
75
  | `AKAN_SERVER_PAGES_SPLITTING` | off | Emit the server pages bundle as chunks instead of one file. Experimental — the memory/latency trade has not been measured on a real app. |
71
76
 
77
+ Two more are read by the dev server for its end-to-end tests only; never set them. `AKAN_DEV_SSR_ARM_DELAY_MS`
78
+ holds the SSR registry's boot build back, and `AKAN_CSR_DEV_APP_WRITE_DELAY_MS` holds a registry's `app.js`
79
+ back after a patch is announced.
80
+
72
81
  ## Observability
73
82
 
74
83
  | variable | default | what it does |
@@ -80,31 +89,36 @@ which is the one case where replacing it again cannot help; it says that too, an
80
89
  ## Several apps at once
81
90
 
82
91
  `akan start a,b` runs one dev host per app under a supervisor, so **every number above multiplies by the number
83
- of apps** — there is no shared builder and no shared RSC worker. Measured on this repo, right after both apps
92
+ of apps** — there is no shared builder and no shared RSC worker. Measured on this repo 8 seconds after both apps
84
93
  finished booting:
85
94
 
86
95
  | process | akan | minimal |
87
96
  |---|---|---|
88
- | dev host | 101MB | 94MB |
89
- | incremental builder | 596MB | 532MB |
90
- | backend | 35MB | 38MB |
91
- | RSC worker | 190MB | 81MB |
92
-
93
- Plus ~51MB for the supervisor itself: **~1.7GB for two apps at their peak.** The builders are almost all of
94
- that, and they are also the part that goes away — `AKAN_DEV_IDLE_SUSPEND_MS` releases each one independently, so
95
- a session where you are editing one app settles to roughly one builder plus ~190MB per idle app. **Multi-app is
96
- sized against idle suspend being on**; setting it to `0` keeps every builder resident for the whole session.
97
+ | dev host | 279MB | 196MB |
98
+ | incremental builder | 182MB | 114MB |
99
+ | backend (gateway and replica) | 123MB | 130MB |
100
+ | RSC worker | 162MB | 75MB |
101
+
102
+ Plus ~75MB for the supervisor itself: **~1.3GB for two apps at rest.** The peak is the boot, not the rest: the
103
+ build workers — the base build, then the SSR registry's — take a whole akan dev host to ~1.9GB and minimal's to
104
+ ~1.4GB, and booted one after the other the two peaked at 2.2GB. The builders are also the part that goes away —
105
+ `AKAN_DEV_IDLE_SUSPEND_MS` releases each one independently, so a session where you are editing one app keeps one
106
+ builder, and every idle app keeps only its dev host, backend and RSC worker. **Multi-app is sized against idle
107
+ suspend being on**; setting it to `0` keeps every builder resident for the whole session.
97
108
 
98
109
  Two things bound the peak rather than the floor:
99
110
 
100
- - **`--concurrency` (default: what the machine allows).** Apps boot in waves, and the next wave starts only
101
- once the previous one reports ready. A cold boot build is the builder's peak, so booting `n` apps at once
102
- means `n` overlapping peaks — which is what OOM-kills a container that would have been fine with them
103
- staggered. Unset, the wave is `min(apps, half the memory budget / 900MB per app, cores / 4)`, never below
104
- one, and the session prints which — so a laptop boots its apps together and a 1.2GB container still
105
- staggers them. The memory budget is the smaller of the host's RAM and `AKAN_MEMORY_LIMIT` / the cgroup
106
- limit; `os.freemem()` is not consulted, because it counts free pages rather than reclaimable ones and
107
- reports ~0.3GB on an idle 48GB laptop.
111
+ - **`--concurrency` (default: what the machine allows).** Apps boot in waves. The next wave starts once each
112
+ app of the previous one serves and its builder's boot builds have settled — the SSR registry's, and CSR's when
113
+ `AKAN_DEV_CSR_REBUILD=1` arms it — since those workers are the largest processes of a boot. An app that never
114
+ reports them starts the next wave 30 seconds after it serves. It shows as ready, and `--open` opens it, as
115
+ soon as it serves. Booting `n` apps at once means `n` overlapping peaks — which is what OOM-kills a container
116
+ that would have been fine with them staggered. Unset, the wave is
117
+ `min(apps, half the memory budget / 1.8GB per app, cores / 4)`, never below one, and the session prints
118
+ which — so a laptop boots its apps together and a small container boots them one at a time. The 1.8GB is
119
+ apps/akan's boot peak across all its processes (measured 1.73–1.9GB). The memory budget is the smaller of
120
+ the host's RAM and `AKAN_MEMORY_LIMIT` / the cgroup limit; `os.freemem()` is not consulted, because it counts
121
+ free pages rather than reclaimable ones and reports ~0.3GB on an idle 48GB laptop.
108
122
  - **`AKAN_MEMORY_LIMIT` is per process, not per session.** Each dev host derives its builder and RSC-worker
109
123
  ceilings from it independently, so a limit sized for one app does not become a budget for four. Divide it
110
124
  yourself, or leave it unset on a laptop. The one place it is read as a session ceiling is the boot wave
@@ -113,18 +127,22 @@ Two things bound the peak rather than the floor:
113
127
 
114
128
  ## Sizing a small sandbox
115
129
 
116
- A worked example, for a 1.2GB container:
130
+ A worked example, for a 1.2GB container running an app the size of `apps/minimal` (apps/akan's boot alone
131
+ peaks near 1.8GB):
117
132
 
118
133
  ```bash
119
134
  AKAN_MEMORY_LIMIT=1200mb # builder gets ~420MB, rsc worker ~660MB
120
135
  AKAN_DEV_IDLE_SUSPEND_MS=300000 # release the builder after 5 idle minutes
121
136
  ```
122
137
 
123
- Two things to expect at that size. The builder crosses 420MB during ordinary work — one route build
124
- costs ~247MB on top of its floor — so it is replaced roughly once per 30s while you keep building, each
138
+ Two things to expect at that size. The builder crosses 420MB during ordinary work — its first route
139
+ build alone takes it to ~540MB — so it is replaced roughly once per 30s while you keep building, each
125
140
  replacement costing a boot build that requests wait through rather than fail. And the build worker's
126
141
  peak is not covered by any of these ceilings; if the kernel OOM-kills it, the dev server survives with a
127
- red build for that generation and the log names the signal.
142
+ red build for that generation and the log names the signal. On a boot whose `.akan` was cleared, the SSR
143
+ registry's first build is such a worker, peaking near 900MB on `apps/minimal`: a killed one leaves no
144
+ registry, and every later save retries that same whole build, so a container that cannot fit it once
145
+ cannot fit it at all.
128
146
 
129
147
  Raising `AKAN_BUILDER_MAX_RSS_MB` above the derived share trades memory for fewer boot builds. Setting
130
148
  it to `0` trades the bound away entirely, which on a container this size means the kernel decides
package/README.ko.md CHANGED
@@ -44,7 +44,7 @@ await runner.build();
44
44
  - Frontend build transform과 RSC/SSR artifact builder.
45
45
  - `@akanjs/cli`가 사용하는 command/script decorator.
46
46
  - AI prompt, guideline, code-generation 지원 utility.
47
- - Capacitor와 mobile release helper.
47
+ - `@akanjs/native` 기반 네이티브 모바일 빌드(iOS, Android)와 스토어 release helper.
48
48
 
49
49
  ## 패키지 경계
50
50
 
@@ -58,7 +58,7 @@ await runner.build();
58
58
 
59
59
  - [Bun](https://bun.sh) `>=1.4.0`
60
60
  - TypeScript
61
- - Optional peer는 Capacitor integration처럼 해당 기능을 사용할 때만 필요합니다.
61
+ - `react`는 optional peer이며, 렌더링하는 기능에서만 필요합니다.
62
62
 
63
63
  ## 라이선스
64
64
 
package/README.md CHANGED
@@ -43,7 +43,8 @@ await runner.build();
43
43
  - Frontend build transforms and RSC/SSR artifact builders.
44
44
  - Command/script decorators used by `@akanjs/cli`.
45
45
  - AI prompt, guideline, and code-generation support utilities.
46
- - Capacitor and mobile release helpers.
46
+ - Native mobile builds on `@akanjs/native` (iOS, Android) and store release helpers.
47
+ - The `akan code` agent engine, the tools it is given, and the shipped akan skills (`@akanjs/devkit/codeAgent`).
47
48
 
48
49
  ## Dev Server Sizing
49
50
 
@@ -62,7 +63,7 @@ defaults, the shares they derive from `AKAN_MEMORY_LIMIT`, and what a small cont
62
63
 
63
64
  - [Bun](https://bun.sh) `>=1.4.0`
64
65
  - TypeScript
65
- - Optional peers are only needed for the features that use them, such as Capacitor integration.
66
+ - `react` is an optional peer, needed only by the features that render.
66
67
 
67
68
  ## License
68
69
 
@@ -1,29 +1,20 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
4
2
  import path from "node:path";
5
3
  import { AbstractDoc } from "./abstractDoc";
6
4
  import { AppExecutor, WorkspaceExecutor } from "./executors";
5
+ import { tempDirs, writeText } from "./testHelpers";
7
6
 
8
- const tempRoots: string[] = [];
7
+ const makeTempRoot = tempDirs("akan-abstract-doc-");
9
8
 
10
9
  const makeApp = async (files: Record<string, string>) => {
11
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-abstract-doc-"));
12
- tempRoots.push(root);
10
+ const root = await makeTempRoot();
13
11
  const appName = "abstractDemo";
14
- for (const [filePath, content] of Object.entries(files)) {
15
- const absolutePath = path.join(root, "apps", appName, filePath);
16
- await mkdir(path.dirname(absolutePath), { recursive: true });
17
- await writeFile(absolutePath, content);
18
- }
12
+ for (const [filePath, content] of Object.entries(files))
13
+ await writeText(path.join(root, "apps", appName, filePath), content);
19
14
  const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "repo" });
20
15
  return AppExecutor.from(workspace, appName);
21
16
  };
22
17
 
23
- afterEach(async () => {
24
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
25
- });
26
-
27
18
  describe("AbstractDoc.kindOf", () => {
28
19
  test("reads the module kind off the sys-relative path", () => {
29
20
  expect(AbstractDoc.kindOf("lib/user/user.abstract.md")).toBe("domain");
package/abstractDoc.ts CHANGED
@@ -1,14 +1,12 @@
1
+ import path from "node:path";
1
2
  import type { SysExecutor } from "./executors";
2
3
 
3
4
  export type AbstractKind = "domain" | "service" | "scalar" | "other";
4
5
 
5
- /** One `*.abstract.md` file: the agent-facing summary a module carries next to its source. */
6
6
  export class AbstractDoc {
7
7
  static readonly suffix = ".abstract.md";
8
- /** `akan quality` warns above this and points at `akan compact`. */
8
+ /** `akan quality` warns above this. */
9
9
  static readonly maxLines = 300;
10
- /** `akan compact` floor, and the line budget it compacts down to, so a compacted file is never a candidate again. */
11
- static readonly compactMinLines = 40;
12
10
  // Abstracts only live in facet folders; globbing the sys root instead would walk node_modules.
13
11
  static readonly #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
14
12
 
@@ -18,7 +16,6 @@ export class AbstractDoc {
18
16
  static lineCountOf(content: string) {
19
17
  return content.split(/\r?\n/).length;
20
18
  }
21
- /** Module kind read off the sys-relative path; anything outside a `lib/` module folder is "other". */
22
19
  static kindOf(filePath: string): AbstractKind {
23
20
  const [root, folder, ...rest] = filePath.split("/");
24
21
  if (root !== "lib" || !folder) return "other";
@@ -27,7 +24,9 @@ export class AbstractDoc {
27
24
  return folder.startsWith("_") ? "service" : "domain";
28
25
  }
29
26
  static async findAll(sys: SysExecutor, { module }: { module?: string | null } = {}) {
30
- const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`)).sort();
27
+ const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`))
28
+ .map((filePath) => filePath.split(path.sep).join("/"))
29
+ .sort();
31
30
  const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
32
31
  if (!module) return docs;
33
32
  return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
@@ -59,8 +58,7 @@ export class AbstractDoc {
59
58
  get lineCount() {
60
59
  return AbstractDoc.lineCountOf(this.content);
61
60
  }
62
- // An AI editor answers in prose when it decides nothing needs changing, and that prose would silently
63
- // replace the abstract — so a candidate must look like a markdown file and be shorter than what it replaces.
61
+ // An AI editor that decides nothing needs changing answers in prose, which must not replace the abstract.
64
62
  canReplaceWith(candidate: string) {
65
63
  const next = candidate.trim();
66
64
  if (!next.startsWith("#")) return false;
@@ -1,6 +1,4 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
2
  import path from "node:path";
5
3
  import {
6
4
  AGENT_BLOCK_END,
@@ -14,6 +12,7 @@ import {
14
12
  upsertAgentBlock,
15
13
  } from "./agentsIndex";
16
14
  import type { RecipeInfo } from "./recipeScanner";
15
+ import { tempDirs, writeText } from "./testHelpers";
17
16
 
18
17
  const button: RecipeInfo = {
19
18
  name: "buttonRecipe",
@@ -79,12 +78,10 @@ describe("upsertAgentBlock / extractAgentBlock", () => {
79
78
  });
80
79
 
81
80
  describe("collectScopeRecipeSources", () => {
81
+ const makeTempRoot = tempDirs("agents-index-");
82
82
  test("collects own + dependency lib recipes, never the framework's", async () => {
83
- const root = await mkdtemp(path.join(tmpdir(), "agents-index-"));
84
- const write = async (rel: string, content: string) => {
85
- await mkdir(path.dirname(path.join(root, rel)), { recursive: true });
86
- await writeFile(path.join(root, rel), content);
87
- };
83
+ const root = await makeTempRoot();
84
+ const write = async (rel: string, content: string) => await writeText(path.join(root, rel), content);
88
85
  await write("apps/minimal/ui/Recipe/appCard.ts", `export const appCard = recipe(tv({ base: "x" }));`);
89
86
  await write("libs/shared/ui/Recipe/panel.ts", `export const panelRecipe = recipe(tv({ base: "y" }));`);
90
87
  await write("pkgs/akanjs/ui/recipe/buttonRecipe.ts", `export const buttonRecipe = recipe(tv({ base: "z" }));`);
package/agentsIndex.ts CHANGED
@@ -1,31 +1,18 @@
1
1
  import type { RecipeInfo, RecipeSource } from "./recipeScanner";
2
2
 
3
- // recipeScanner 는 상단에서 typescript(~65MB)를 끌어온다. 이 모듈은 executors(CLI 엔트리 그래프)에
4
- // 상주하므로 스캔 스택은 첫 사용 시점에 지연 로드한다 — 정적 import 로 되돌리면 entryModuleGraph 테스트가 깨진다.
3
+ // Lazy: recipeScanner loads `typescript` (~65MB) and this module sits in the CLI entry graph (entryModuleGraph test).
5
4
  let recipeScannerLoad: Promise<typeof import("./recipeScanner")> | null = null;
6
5
  const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner"));
7
6
 
8
- /**
9
- * agentsIndex — 스코프별 에이전트 색인의 단일 렌더러.
10
- *
11
- * 색인은 소유 경계로 쪼갠다: 루트 AGENTS.md 는 프레임워크(akanjs/ui) 레시피만 싣고, 각 앱/lib 은
12
- * 자기 스코프에서 import 가능한 레시피(own + 의존 lib)를 자기 AGENTS.md 에 싣는다. 항상 로드되는
13
- * 컨텍스트가 앱 수에 비례해 커지는 것과, import 불가능한 이웃 앱 레시피가 환상을 유발하는 것을 막는다.
14
- *
15
- * 신선도는 두 지점이 보장한다: `SysExecutor.scan(write)` 가 재생성하고(akan sync/build/start 가 전부
16
- * 지나가는 길목), `akan lint` 가 스캔 결과와 커밋된 블록을 비교해 stale 이면 실패시킨다.
17
- */
7
+ //* Split by ownership (root: framework recipes; each app/lib: what it may import), so always-loaded context does not
8
+ //* grow with the app count and no unimportable neighbour recipe is listed.
18
9
 
19
10
  export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
20
11
  export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
21
12
 
22
13
  const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
23
14
 
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
- */
15
+ /** Nothing re-runs `akan agent install` on its own, so `akan doctor` compares this stamp against the installed devkit. */
29
16
  export const stampBlockVersion = (block: string, version: string): string =>
30
17
  `${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
31
18
 
@@ -36,7 +23,7 @@ export const extractBlockVersion = (content: string): string | null =>
36
23
  /** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
37
24
  export const readDevkitVersion = async (): Promise<string | null> => {
38
25
  const { readFile } = await import("node:fs/promises");
39
- const { getDirname } = await import("./getDirname");
26
+ const { getDirname } = await import("./fileSys");
40
27
  try {
41
28
  const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
42
29
  return (JSON.parse(raw) as { version?: string }).version ?? null;
@@ -97,10 +84,7 @@ export const renderRecipeEntries = (recipes: RecipeInfo[]): string => {
97
84
  .join("\n\n");
98
85
  };
99
86
 
100
- /**
101
- * Every recipe source importable from the scope: its own `ui/Recipe/` plus each dependency lib's.
102
- * Framework recipes are excluded on purpose — they live in the root AGENTS.md, valid for every scope.
103
- */
87
+ /** Own `ui/Recipe/` plus each dependency lib's; framework recipes live in the root AGENTS.md instead. */
104
88
  export const collectScopeRecipeSources = async (
105
89
  workspaceRoot: string,
106
90
  scope: AgentsIndexScope,
@@ -1,34 +1,24 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { realpath, rm, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { Logger } from "akanjs/common";
6
5
  import type { App } from "../commandDecorators";
6
+ import { tempDirs, writeText } from "../testHelpers";
7
7
  import { BackendImportGraph } from "./BackendImportGraph";
8
8
  import { filesChangedSince } from "./devHostPolicy";
9
9
 
10
10
  describe("BackendImportGraph", () => {
11
- const tempRoots: string[] = [];
11
+ const makeTempRoot = tempDirs("akan-devkit-graph-");
12
12
 
13
13
  const makeGraph = async (files: Record<string, string>) => {
14
- // Realpath, not the mkdtemp path: `Bun.resolveSync` returns real paths, and on macOS `/var/folders`
15
- // is a symlink, so an unresolved root makes every resolved import look like it escapes the workspace.
16
- const workspaceRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "akan-devkit-graph-")));
17
- tempRoots.push(workspaceRoot);
14
+ // Realpath: `Bun.resolveSync` returns real paths, and macOS `/var/folders` is a symlink.
15
+ const workspaceRoot = await realpath(await makeTempRoot());
18
16
  const cwdPath = path.join(workspaceRoot, "apps/demo");
19
- for (const [rel, source] of Object.entries(files)) {
20
- const filePath = path.join(cwdPath, rel);
21
- await mkdir(path.dirname(filePath), { recursive: true });
22
- await writeFile(filePath, source);
23
- }
17
+ for (const [rel, source] of Object.entries(files)) await writeText(path.join(cwdPath, rel), source);
24
18
  const app = { cwdPath, workspace: { workspaceRoot } } as unknown as App;
25
19
  return { graph: new BackendImportGraph(app, new Logger("test")), cwdPath };
26
20
  };
27
21
 
28
- afterEach(async () => {
29
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
30
- });
31
-
32
22
  test("walks the backend entrypoints' import graph", async () => {
33
23
  const { graph, cwdPath } = await makeGraph({
34
24
  "main.ts": 'import "./server";\n',
@@ -51,7 +41,6 @@ describe("BackendImportGraph", () => {
51
41
  await graph.refresh();
52
42
  expect(graph.has(path.join(cwdPath, "lib/added.ts"))).toBe(false);
53
43
 
54
- // The scan cache is keyed on (mtimeMs, size), so the rewrite must invalidate it.
55
44
  await writeFile(path.join(cwdPath, "server.ts"), 'import "./lib/added";\nexport default 1;\n');
56
45
 
57
46
  await graph.refresh();
@@ -81,8 +70,7 @@ describe("BackendImportGraph", () => {
81
70
  await graph.refresh();
82
71
  const before = await graph.fingerprint();
83
72
 
84
- // The builder is gone here, so no watcher event exists for this save — which is the whole reason
85
- // the stamps are taken. `mtimeMs` has a coarse clock on Linux, so the size has to move too.
73
+ // `mtimeMs` has a coarse clock on Linux, so the size has to move too.
86
74
  await writeFile(path.join(cwdPath, "lib/handler.ts"), "export const handler = () => 'changed';\n");
87
75
 
88
76
  expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
@@ -96,11 +84,9 @@ describe("BackendImportGraph", () => {
96
84
  });
97
85
  await graph.refresh();
98
86
  const before = await graph.fingerprint();
99
- // A recycle with no edit in it is the common case, and it must not cost a backend restart.
100
87
  expect(filesChangedSince(before, await graph.fingerprint())).toEqual([]);
101
88
 
102
89
  await rm(path.join(cwdPath, "lib/handler.ts"));
103
- // Deleted counts as changed: the backend is still running what used to be there.
104
90
  expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
105
91
  });
106
92
 
@@ -22,14 +22,7 @@ export class BackendImportGraph {
22
22
  readonly #jsTranspiler = new Bun.Transpiler({ loader: "js" });
23
23
  readonly #jsxTranspiler = new Bun.Transpiler({ loader: "jsx" });
24
24
  #files = new Set<string>();
25
- /**
26
- * `refresh()` runs on every server-side save and on every dev-host recycle, and re-reading plus
27
- * re-parsing files that did not change is the whole cost of it. Keyed on (mtimeMs, size).
28
- *
29
- * Specifiers are cached, not resolved paths: creating a file can change what an *unchanged* importer's
30
- * specifier resolves to, and `Bun.resolveSync` is cheap next to a read plus a transpiler scan. Only
31
- * the scan result is retained — never the source text.
32
- */
25
+ // Specifiers, not resolved paths: a new file can change what an unchanged importer's specifier resolves to.
33
26
  #scanCache = new Map<string, { mtimeMs: number; size: number; specifiers: Bun.Import[] }>();
34
27
  #ready = false;
35
28
  #lastRefreshSucceeded = false;
@@ -51,17 +44,6 @@ export class BackendImportGraph {
51
44
  return this.#files.has(path.resolve(file));
52
45
  }
53
46
 
54
- /**
55
- * Stamp every file the backend runs, so a caller can ask later what moved.
56
- *
57
- * Taken when the builder goes away and compared when its replacement is up, because nothing watches
58
- * the tree in between: the departing builder's watcher left with it, and the replacement's index
59
- * primes from the disk it finds, so an edit that lands in the gap is *baseline* to it and is never
60
- * reported at all. The client half of such an edit is rescued by the replacement's boot build; the
61
- * backend half is a server left running code that no longer exists, with nothing on screen to say so.
62
- *
63
- * One `stat` per graph file, against a gap that costs a whole boot build anyway.
64
- */
65
47
  async fingerprint(): Promise<SourceFingerprints> {
66
48
  const stamps = await Promise.all(
67
49
  [...this.#files].map(async (file) => {
@@ -117,7 +99,6 @@ export class BackendImportGraph {
117
99
  return files;
118
100
  }
119
101
 
120
- /** Null when the file is gone, which is the existence check the walk used to make separately. */
121
102
  async #importsOf(file: string): Promise<Bun.Import[] | null> {
122
103
  const stats = await stat(file).catch(() => null);
123
104
  if (!stats?.isFile()) return null;
@@ -1,18 +1,71 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import { IncrementalBuilderHost } from "../incrementalBuilder";
4
+ import { createTempApp, isolateEnv, tempRoots, writeText } from "../testHelpers";
2
5
  import { AkanAppHost } from "./akanApp.host";
6
+ import type { DevHostEvent } from "./devHostPolicy";
3
7
 
4
8
  describe("readProcessRssBytes", () => {
5
9
  test("reads this process's own rss", async () => {
6
10
  const rssBytes = await AkanAppHost.readProcessRssBytes(process.pid);
7
11
  if (rssBytes === null) throw new Error("expected to read this process's own rss");
8
- // Loose bounds on purpose: the point is that it read a real number from the OS, not which number.
9
12
  expect(rssBytes).toBeGreaterThan(1024 * 1024);
10
13
  expect(rssBytes).toBeLessThan(64 * 1024 * 1024 * 1024);
11
14
  });
12
15
 
13
- // Null rather than 0, because callers must treat an unreadable pid as "no new information" — a 0
14
- // would read as "settled below the ceiling" and cancel a recycle that should happen.
16
+ // A 0 would read as "settled below the ceiling" and cancel a recycle that should happen.
15
17
  test("returns null for a pid that does not exist", async () => {
16
18
  expect(await AkanAppHost.readProcessRssBytes(2_147_483_646)).toBeNull();
17
19
  });
18
20
  });
21
+
22
+ describe("a gateway whose replica crash-loops", () => {
23
+ isolateEnv({ AKAN_PUBLIC_REPO_NAME: "repo", AKAN_PUBLIC_SERVE_DOMAIN: "localhost", AKAN_PUBLIC_ENV: "local" });
24
+ const track = tempRoots();
25
+ const crashLoop = "Backend replica 0/federation failed 3 consecutive boots; waiting for a code change to retry: boom";
26
+ const readyBuilder = {
27
+ status: "ready",
28
+ patcherOff: false,
29
+ start: ({ onReady }: { onReady?: () => void }) => onReady?.(),
30
+ send: () => true,
31
+ stop: () => undefined,
32
+ } as unknown as IncrementalBuilderHost;
33
+
34
+ test("fails the dev server at once, and leaves it running for the fix", async () => {
35
+ const { root, app } = track(await createTempApp("demo"));
36
+ await writeText(
37
+ path.join(root, "apps/demo/main.ts"),
38
+ [
39
+ `process.send?.({ type: "build-status", data: { generation: -1, phase: "backend", ok: false, files: [], message: ${JSON.stringify(crashLoop)} } });`,
40
+ "setInterval(() => undefined, 1_000);",
41
+ "",
42
+ ].join("\n"),
43
+ );
44
+ const create = IncrementalBuilderHost.create;
45
+ IncrementalBuilderHost.create = async () => readyBuilder;
46
+ const events: DevHostEvent[] = [];
47
+ const failed = Promise.withResolvers<void>();
48
+ const host = new AkanAppHost(app, {
49
+ env: { ...process.env } as Record<string, string>,
50
+ onDevEvent: (event) => {
51
+ events.push(event);
52
+ if ("state" in event && event.state === "failed") failed.resolve();
53
+ },
54
+ });
55
+ let beforeStop: DevHostEvent[] = [];
56
+ try {
57
+ await host.start();
58
+ await failed.promise;
59
+ await Bun.sleep(300);
60
+ beforeStop = [...events];
61
+ } finally {
62
+ await host.stop();
63
+ IncrementalBuilderHost.create = create;
64
+ }
65
+
66
+ expect(beforeStop).toEqual([
67
+ { app: "demo", state: "starting" },
68
+ { app: "demo", state: "failed", detail: crashLoop },
69
+ ]);
70
+ });
71
+ });