@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.21

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 (289) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +3 -2
  3. package/abstractDoc.test.ts +6 -15
  4. package/abstractDoc.ts +6 -8
  5. package/agentsIndex.test.ts +4 -7
  6. package/agentsIndex.ts +6 -22
  7. package/akanApp/BackendImportGraph.test.ts +8 -22
  8. package/akanApp/BackendImportGraph.ts +1 -20
  9. package/akanApp/akanApp.host.test.ts +1 -3
  10. package/akanApp/akanApp.host.ts +82 -254
  11. package/akanApp/devHostPolicy.test.ts +10 -34
  12. package/akanApp/devHostPolicy.ts +12 -98
  13. package/akanConfig/akanConfig.test.ts +142 -106
  14. package/akanConfig/akanConfig.ts +147 -191
  15. package/akanConfig/types.ts +4 -3
  16. package/akanContext.ts +110 -160
  17. package/akanMcpContract.ts +28 -73
  18. package/appSelectionMemory.ts +1 -4
  19. package/applicationBuildReporter.ts +37 -5
  20. package/applicationBuildRunner.test.ts +5 -3
  21. package/applicationBuildRunner.ts +54 -61
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +32 -30
  24. package/artifact/implicitRootLayout.ts +58 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +32 -1
  28. package/biomeBase.ts +2 -5
  29. package/cloud/cloudApi.test.ts +202 -0
  30. package/cloud/cloudApi.ts +77 -39
  31. package/cloud/configLock.test.ts +64 -0
  32. package/cloud/configLock.ts +96 -0
  33. package/cloud/constants.ts +26 -5
  34. package/cloud/globalConfig.ts +70 -32
  35. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  36. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  37. package/codeAgent/agent/CodeAgent.ts +551 -0
  38. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  39. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  40. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  41. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  42. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  43. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  44. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  45. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  46. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  47. package/codeAgent/agent/CodeMailbox.ts +148 -0
  48. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  49. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  50. package/codeAgent/agent/SubagentPool.ts +214 -0
  51. package/codeAgent/agent/akanCodeModel.ts +47 -0
  52. package/codeAgent/agent/akanCodePaths.ts +31 -0
  53. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  54. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  55. package/codeAgent/agent/codeAgent.test.ts +178 -0
  56. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  57. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  58. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  59. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  60. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  61. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  62. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  63. package/codeAgent/agent/consoleToStderr.ts +8 -0
  64. package/codeAgent/agent/jsonLines.ts +29 -0
  65. package/codeAgent/agent/stringArg.ts +5 -0
  66. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  67. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  68. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  69. package/codeAgent/feedback/PreviewView.ts +95 -0
  70. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  71. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  72. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  73. package/codeAgent/index.ts +34 -0
  74. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  75. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  76. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  77. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  78. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  79. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  80. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  81. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  82. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  83. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  84. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  85. package/codeAgent/tools/AkanCli.ts +50 -0
  86. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  87. package/codeAgent/tools/AkanEditScope.ts +48 -0
  88. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  89. package/codeAgent/tools/AkanToolPack.ts +92 -0
  90. package/codeAgent/tools/AkanVerifier.ts +48 -0
  91. package/codeAgent/tools/AskToolPack.ts +99 -0
  92. package/codeAgent/tools/MailToolPack.ts +76 -0
  93. package/codeAgent/tools/McpClient.ts +187 -0
  94. package/codeAgent/tools/McpOAuth.ts +233 -0
  95. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  96. package/codeAgent/tools/McpServerConfig.ts +129 -0
  97. package/codeAgent/tools/McpSignIn.ts +158 -0
  98. package/codeAgent/tools/McpTokenStore.ts +81 -0
  99. package/codeAgent/tools/McpToolPack.ts +199 -0
  100. package/codeAgent/tools/SessionToolPack.ts +91 -0
  101. package/codeAgent/tools/WebToolPack.ts +147 -0
  102. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  103. package/codeAgent/tools/askToolPack.test.ts +116 -0
  104. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  105. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  106. package/codegenLock.test.ts +4 -13
  107. package/codegenLock.ts +3 -17
  108. package/commandDecorators/argMeta.ts +1 -5
  109. package/commandDecorators/camelToKebabCase.ts +1 -0
  110. package/commandDecorators/command.ts +46 -102
  111. package/commandDecorators/commandBuilder.ts +30 -61
  112. package/commandDecorators/commandDecorators.test.ts +2 -10
  113. package/commandDecorators/dependencyBuilder.ts +10 -1
  114. package/commandDecorators/helpFormatter.ts +68 -145
  115. package/commandDecorators/index.ts +0 -2
  116. package/commandDecorators/targetMeta.ts +9 -7
  117. package/createTunnel.ts +7 -14
  118. package/dependencyScanner.test.ts +4 -16
  119. package/dependencyScanner.ts +31 -80
  120. package/devkitUtils.test.ts +48 -20
  121. package/executors.linkFallback.test.ts +103 -0
  122. package/executors.test.ts +132 -106
  123. package/executors.ts +195 -496
  124. package/fileEditor.ts +9 -37
  125. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  126. package/fileSys.ts +6 -8
  127. package/formSetterScanner.ts +3 -16
  128. package/frontendBuild/allRoutesBuilder.ts +7 -21
  129. package/frontendBuild/autoImportSync.test.ts +27 -38
  130. package/frontendBuild/autoImportSync.ts +31 -65
  131. package/frontendBuild/buildRouteClient.test.ts +62 -37
  132. package/frontendBuild/bundleDefine.ts +12 -0
  133. package/frontendBuild/clientBuildTypes.ts +5 -36
  134. package/frontendBuild/clientEntriesBundler.ts +9 -40
  135. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  136. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  137. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  138. package/frontendBuild/cssCandidateCache.ts +6 -41
  139. package/frontendBuild/cssCompiler.ts +29 -78
  140. package/frontendBuild/cssImportResolver.ts +6 -19
  141. package/frontendBuild/devChangePlanner.ts +7 -10
  142. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  143. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  144. package/frontendBuild/fontOptimizer.test.ts +6 -14
  145. package/frontendBuild/fontOptimizer.ts +15 -72
  146. package/frontendBuild/fontPruner.test.ts +5 -10
  147. package/frontendBuild/fontPruner.ts +5 -20
  148. package/frontendBuild/frontendBuild.test.ts +46 -69
  149. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  150. package/frontendBuild/hmrWatcher.test.ts +8 -32
  151. package/frontendBuild/hmrWatcher.ts +14 -71
  152. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  153. package/frontendBuild/precompressArtifacts.ts +5 -18
  154. package/frontendBuild/routeClientBuilder.ts +23 -46
  155. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  156. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  157. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  158. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  159. package/frontendBuild/styleContract.ts +1 -8
  160. package/frontendBuild/themeValidator.ts +4 -26
  161. package/frontendBuild/watchRootResolver.test.ts +23 -42
  162. package/frontendBuild/watchRootResolver.ts +5 -27
  163. package/getRelatedCnsts.ts +12 -92
  164. package/incrementalBuilder/buildBatch.proc.ts +29 -58
  165. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  166. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  167. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  168. package/incrementalBuilder/builderChannel.test.ts +24 -49
  169. package/incrementalBuilder/builderChannel.ts +6 -37
  170. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  171. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  172. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  173. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  174. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  175. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  176. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  177. package/index.ts +5 -19
  178. package/integration/devResourceProbe.ts +50 -56
  179. package/integration/devStability.integration.test.ts +47 -194
  180. package/integration/devStabilityHarness.test.ts +13 -28
  181. package/integration/devStabilityHarness.ts +65 -330
  182. package/integration/ssrMemoryProbe.ts +41 -167
  183. package/libSource.test.ts +4 -15
  184. package/libSource.ts +19 -31
  185. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  186. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  187. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  188. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  189. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  190. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  191. package/lint/gritRules.test.ts +8 -24
  192. package/lint/no-deep-internal-import.grit +2 -2
  193. package/lint/no-document-cookie.grit +29 -0
  194. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  195. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  196. package/lint/no-web-storage.grit +30 -0
  197. package/linter.test.ts +1 -2
  198. package/linter.ts +26 -116
  199. package/mobile/index.ts +6 -0
  200. package/mobile/mobileTarget.ts +16 -13
  201. package/mobile/nativeApi.test.ts +30 -0
  202. package/mobile/nativeApi.ts +40 -0
  203. package/mobile/nativeApp.test.ts +85 -0
  204. package/mobile/nativeApp.ts +210 -0
  205. package/mobile/nativeConfig.test.ts +199 -0
  206. package/mobile/nativeConfig.ts +184 -0
  207. package/mobile/nativeWebDir.test.ts +80 -0
  208. package/mobile/nativeWebDir.ts +62 -0
  209. package/mobile/placeholderAppId.test.ts +13 -0
  210. package/mobile/placeholderAppId.ts +16 -0
  211. package/mobile/usageDescriptions.test.ts +27 -0
  212. package/mobile/usageDescriptions.ts +24 -0
  213. package/package.json +10 -10
  214. package/packageExports.test.ts +3 -18
  215. package/packageExportsMap.ts +3 -19
  216. package/piDependencyPins.test.ts +46 -0
  217. package/platformTest/BunTestReport.test.ts +60 -0
  218. package/platformTest/BunTestReport.ts +65 -0
  219. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  220. package/platformTest/IdleWatchedProcess.ts +73 -0
  221. package/platformTest/LinuxDockerTarget.ts +105 -0
  222. package/platformTest/PlatformTestRun.test.ts +64 -0
  223. package/platformTest/PlatformTestRun.ts +141 -0
  224. package/platformTest/PlatformTestTarget.ts +142 -0
  225. package/platformTest/TestSnapshot.test.ts +72 -0
  226. package/platformTest/TestSnapshot.ts +103 -0
  227. package/platformTest/WindowsSshTarget.ts +151 -0
  228. package/prompter.ts +3 -13
  229. package/qualityScanner.test.ts +9 -26
  230. package/qualityScanner.ts +200 -286
  231. package/recipeScanner.test.ts +7 -12
  232. package/recipeScanner.ts +9 -38
  233. package/repoIdentity.ts +2 -9
  234. package/routeSourceValidator.test.ts +0 -1
  235. package/routeSourceValidator.ts +7 -42
  236. package/scanInfo.ts +91 -225
  237. package/selectModel.ts +1 -2
  238. package/semver.ts +3 -5
  239. package/slicePlanner.test.ts +6 -24
  240. package/slicePlanner.ts +11 -32
  241. package/spinner.test.ts +3 -8
  242. package/spinner.ts +4 -16
  243. package/sshTunnel.ts +1 -6
  244. package/ssrScanner.test.ts +1 -6
  245. package/ssrScanner.ts +103 -113
  246. package/stripAnsi.ts +3 -0
  247. package/subspace.test.ts +30 -25
  248. package/subspace.ts +81 -115
  249. package/subspaceConfig.ts +11 -9
  250. package/testHelpers.ts +194 -0
  251. package/transforms/asyncDefaultExportDetector.ts +3 -11
  252. package/transforms/barrelAnalyzer.ts +43 -135
  253. package/transforms/barrelImportsPlugin.ts +38 -175
  254. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  255. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  256. package/transforms/moduleSyntax.ts +32 -0
  257. package/transforms/rscUseClientTransform.ts +35 -20
  258. package/transforms/transforms.test.ts +153 -44
  259. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  260. package/transforms/useClientBundlePlugin.ts +2 -22
  261. package/typeChecker.ts +7 -63
  262. package/types.ts +1 -0
  263. package/uploadRelease.ts +10 -25
  264. package/workflow/artifacts.ts +18 -0
  265. package/workflow/executor.test.ts +1 -12
  266. package/workflow/executor.ts +101 -110
  267. package/workflow/index.ts +0 -1
  268. package/workflow/moduleIndex.test.ts +4 -15
  269. package/workflow/moduleIndex.ts +9 -49
  270. package/workflow/plan.ts +18 -25
  271. package/workflow/render.ts +80 -96
  272. package/workflow/rolloutGate.test.ts +1 -1
  273. package/workflow/source.ts +52 -69
  274. package/workflow/sourceAst.ts +50 -0
  275. package/workspaceLayout.test.ts +13 -3
  276. package/workspaceLayout.ts +20 -23
  277. package/abstractCompactor.ts +0 -112
  278. package/aiEditor.test.ts +0 -60
  279. package/aiEditor.ts +0 -387
  280. package/applicationReleasePackager.ts +0 -209
  281. package/capacitorApp.test.ts +0 -372
  282. package/capacitorApp.ts +0 -1648
  283. package/commandDecorators/commandMeta.ts +0 -7
  284. package/commandDecorators/types.ts +0 -10
  285. package/getDirname.ts +0 -4
  286. package/guideline.ts +0 -19
  287. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  288. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  289. package/workflow/primitive.ts +0 -59
@@ -3,9 +3,7 @@ import { DevGeneratedIndexSync } from "../frontendBuild";
3
3
  import { DevStabilityHarness, type DevStabilityHmrProbe } from "./devStabilityHarness";
4
4
 
5
5
  const integrationEnabled = process.env.AKAN_DEV_STABILITY_INTEGRATION === "1";
6
- // Every test here boots a real dev server, and contention pushes a cold boot from ~3s to 21-55s
7
- // (`05-phase1-results.md`). At 120s a loaded machine ran out of budget mid-restart and reported it as a
8
- // product failure, which is the exact ambiguity this file exists to remove.
6
+ // Contention pushes a cold dev-server boot from ~3s to nearly a minute; 120s ran out mid-restart.
9
7
  const INTEGRATION_TIMEOUT_MS = 180_000;
10
8
  const MB = 1024 * 1024;
11
9
  const harnesses: DevStabilityHarness[] = [];
@@ -38,15 +36,7 @@ const isBuildStatus =
38
36
  "status" in msg &&
39
37
  msg.status === status;
40
38
 
41
- /**
42
- * Assert an HMR message arrives — unless the socket dropped while waiting.
43
- *
44
- * The hub does not replay (`akanjs/server/hmr/wsHub.ts`): anything published while the probe was
45
- * reconnecting is simply gone, so a miss across a reconnect says nothing about the product. A real browser
46
- * covers the same gap by reloading when the `hello` buildId moved, not by expecting the message. Under
47
- * parallel load the socket flaps repeatedly, and this is what that looks like from the probe:
48
- * `socket=closed reconnects=5 since-mark=[build-status,hello,hello,rsc-refresh,…]`.
49
- */
39
+ // The hub does not replay (`akanjs/server/hmr/wsHub.ts`), so a miss across a probe reconnect proves nothing.
50
40
  const expectHmrMessage = async (
51
41
  probe: DevStabilityHmrProbe,
52
42
  mark: number,
@@ -117,21 +107,12 @@ const waitForProcessesGone = async (pids: number[], timeoutMs = 15_000): Promise
117
107
  return false;
118
108
  };
119
109
 
120
- /**
121
- * `cleanup()` waits up to 3s for a SIGTERM'd dev host to exit and shells out to `ps` twice, and on a
122
- * loaded machine that overruns Bun's default 5s hook budget. A hook timeout fails the test that had
123
- * already passed, which reads exactly like a product failure — two of the five failures in a 3-way
124
- * parallel run were this and nothing else.
125
- */
110
+ // `cleanup()` can overrun Bun's default 5s hook budget, and a hook timeout fails the already-passed test.
126
111
  const HOOK_TIMEOUT_MS = 60_000;
127
112
 
128
113
  beforeAll(async () => {
129
114
  if (!integrationEnabled) return;
130
- // A run killed mid-test (a `-t` filter interrupted, an editor stopping the runner) leaves its fixture
131
- // in `apps/` and its dev host running. Both interfere with every later run: the process holds ports
132
- // and rebuilds a deleted app, and the directory shifts the app index every port prediction derives
133
- // from. Only fixtures whose owning test pid is gone are swept, so this is safe with a suite running
134
- // alongside.
115
+ // A killed run leaves its fixture and dev host behind, holding ports and shifting every app index.
135
116
  const swept = await DevStabilityHarness.sweepAbandonedFixtures(DevStabilityHarness.defaultWorkspaceRoot);
136
117
  if (swept.length) console.info(`[harness] swept ${swept.length} abandoned fixture(s): ${swept.join(", ")}`);
137
118
  }, HOOK_TIMEOUT_MS);
@@ -143,14 +124,8 @@ afterEach(async () => {
143
124
  afterAll(() => {
144
125
  if (!integrationEnabled) return;
145
126
  const { edits, retried } = DevStabilityHarness.editStats();
146
- // Reported on purpose, every run. A retry means a real save produced no rebuild whatsoever — Bun's
147
- // dropped `fs.watch` event (`local/optimize-resource/06-watcher-dropped-event.md`), which users hit too
148
- // through format-on-save and save-all. A non-zero number here means this suite is passing *around* a
149
- // product bug rather than because it is absent, and it is the signal for fixing that bug properly.
127
+ // A retry is a save Bun's `fs.watch` dropped — a product bug users hit too, which this suite passes around.
150
128
  console.info(`[harness] ${edits} observed edit(s), ${retried} needed a retry after a dropped watch event`);
151
- // Every wait is individually bounded but their budgets sum past the per-test timeout, so a loaded round
152
- // can kill a test without any single wait failing — and Bun then reports only "this test timed out". These
153
- // are the numbers that say how close a green run came, and which step to look at when one dies.
154
129
  const slowest = DevStabilityHarness.waitStats();
155
130
  if (slowest.length)
156
131
  console.info(`[harness] slowest waits: ${slowest.map((wait) => `${wait.ms}ms ${wait.label}`).join(" | ")}`);
@@ -227,13 +202,7 @@ describe("dev stability integration harness", () => {
227
202
  );
228
203
  const generation = evidence[1];
229
204
  await host.waitForLogSince(mark, new RegExp(`\\[backend-reload\\].*generation=${generation}`));
230
- // Asserted backend-side, not through the probe. A shared edit restarts the backend, which closes the
231
- // socket the probe opened; the probe reconnects, but a reconnect does not replay what was published
232
- // while it was down, and a real browser handles that by reloading when the `hello` buildId moved
233
- // (`akanjs/server/hmr/clientScript.ts`) rather than by expecting the message. Requiring a probe
234
- // message here only held while the client rebuild happened to finish before the restart killed the
235
- // connection — a race this test lost the moment builds moved into a worker process and took ~240ms
236
- // longer to start.
205
+ // Backend-side: the restart closes the probe's socket, and a reconnect does not replay what it missed.
237
206
  await host.waitForLogSince(mark, new RegExp(`\\[SSR\\] pages-updated.*generation=${generation}`));
238
207
  await harness.waitForHttpText("updated-shared-marker");
239
208
  hmr?.close();
@@ -265,8 +234,7 @@ export const dictionary = serviceDictionary(["en", "ko"])
265
234
  });
266
235
  `,
267
236
  ),
268
- // As with the config edit: each attempt recycles the builder and the backend, so patience is cheaper
269
- // than a retry.
237
+ // Each attempt recycles the builder and the backend, so patience is cheaper than a retry.
270
238
  { evidence: /\[dev-plan\].*actions=.*restart-builder/, attempts: 2, evidenceTimeoutMs: 30_000 },
271
239
  );
272
240
 
@@ -297,8 +265,7 @@ const config: AppConfig = { externalLibs: [] };
297
265
  export default config;
298
266
  `,
299
267
  ),
300
- // Re-applying this edit is expensive — every attempt restarts the whole dev host — so wait longer
301
- // before concluding the event was dropped rather than merely slow.
268
+ // Every attempt restarts the whole dev host, so wait longer before calling the event dropped.
302
269
  { evidence: /\[dev-plan\].*actions=.*restart-dev-host/, attempts: 2, evidenceTimeoutMs: 30_000 },
303
270
  );
304
271
 
@@ -338,8 +305,7 @@ export default config;
338
305
  await harness.waitForHttpText("initial-client-marker");
339
306
  const recoveryHmrMark = hmr?.mark() ?? 0;
340
307
 
341
- // The edit that used to be dropped: it lands right after the failed build's write burst, and in a
342
- // 3-way parallel run all three shards timed out here on 60s of completely empty log output.
308
+ // Lands right after the failed build's write burst, where Bun's watcher drops events.
343
309
  const { mark: recoveryMark } = await harness.editUntilSeen(host, (attempt) =>
344
310
  harness.writeFile(
345
311
  "ui/ClientMarker.tsx",
@@ -359,8 +325,7 @@ export default config;
359
325
  integrationTest("barrel add/delete includes generated indexes in watch generation", async () => {
360
326
  const harness = await createHarness();
361
327
  const sync = new DevGeneratedIndexSync({ workspaceRoot: harness.workspaceRoot });
362
- // `common` barrels export camelCase names; `ui` barrels export PascalCase component names. Each facet's
363
- // fixture file must follow its own casing convention or the barrel deliberately skips it.
328
+ // Each facet's barrel skips a file that breaks its casing convention (`common` camelCase, `ui` PascalCase).
364
329
  const facets = [
365
330
  { facet: "common", moduleName: "tmpExample", fileName: "tmpExample.ts", exportName: "commonTmpExample" },
366
331
  { facet: "ui", moduleName: "TmpExample", fileName: "TmpExample.tsx", exportName: "TmpExample" },
@@ -398,8 +363,7 @@ export default config;
398
363
  integrationTest("backend boot failure stops the crash loop, surfaces build-status, and recovers on fix", async () => {
399
364
  const harness = await createHarness();
400
365
  const host = await harness.startHost();
401
- // The service file is part of the generated server graph (`akan start` regenerates server.ts
402
- // from lib/), so a module-level throw here breaks every replica boot.
366
+ // `akan start` regenerates server.ts from lib/, so a module-level throw here breaks every replica boot.
403
367
  const { mark: failureMark } = await harness.editUntilSeen(host, (attempt) =>
404
368
  harness.writeFile(
405
369
  "lib/_fixture/fixture.service.ts",
@@ -412,9 +376,7 @@ throw new Error("intentional-backend-boot-crash-${attempt}");
412
376
  ),
413
377
  );
414
378
 
415
- // The gateway abandons the replica after three failed boots instead of retrying forever...
416
379
  await host.waitForLogSince(failureMark, /\[child-crash-loop\].*failed 3 consecutive boots/);
417
- // ...and the failure reaches the dev host's build-status pipeline (HMR overlay path).
418
380
  await host.waitForLogSince(failureMark, /\[build-status\].*phase=backend.*ok=false/);
419
381
  expect(host.proc.killed).toBe(false);
420
382
 
@@ -451,8 +413,7 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
451
413
 
452
414
  process.kill(gatewayPid, "SIGKILL");
453
415
 
454
- // Children must notice the closed IPC channel and exit instead of orphaning (they would
455
- // otherwise keep holding their ws ports and break every subsequent boot).
416
+ // An orphaned child would keep holding its ws port and break every later boot.
456
417
  expect(await waitForProcessesGone(childPids)).toBe(true);
457
418
 
458
419
  await host.waitForLogSince(mark, /backend ready pid=(\d+)|AkanApp gateway is running on port/);
@@ -467,8 +428,7 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
467
428
  integrationTest("occupied preferred ws port falls back to an ephemeral port and stays bootable", async () => {
468
429
  const harness = await createHarness();
469
430
  const port = await harness.resolvePort();
470
- // The gateway assigns child 0 the deterministic ws port `port + 10_000`; occupy it up front
471
- // the way an orphaned replica from a killed run would.
431
+ // Child 0's deterministic ws port, held the way an orphaned replica from a killed run would hold it.
472
432
  const blocker = Bun.serve({ port: port + 10_000, fetch: () => new Response("occupied") });
473
433
  try {
474
434
  const host = await harness.startHost();
@@ -491,34 +451,12 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
491
451
  });
492
452
  });
493
453
 
494
- /**
495
- * Resource budgets for `akan start`. A dev sandbox holds this process tree for a whole session, so
496
- * every regression here multiplies by the number of tenants. Budgets are deliberately loose (they
497
- * carry headroom over the measured values) — they exist to catch a *reintroduced* eager import or an
498
- * unbounded per-save ratchet, not to pin exact numbers. Raising one should be a visible diff.
499
- *
500
- * Deliberately two tests, not one per property: each boots a full dev server, and four boots after
501
- * the eleven tests above pushed cold boot from 3s to 21-55s through sheer machine contention, which
502
- * silently exhausted the waits and looked like product failures. Both tests therefore assert several
503
- * related properties against a single boot, with explicit generous waits. Run the block on its own
504
- * (`-t "dev resource budgets"`) when timing matters.
505
- *
506
- * **This block is not parallel-safe, and cannot be made so.** It asserts *absolute* resident memory, so
507
- * several dev servers competing for the machine changes what the number means rather than adding noise
508
- * around it — a 3-way parallel run reported 178MB against the 120MB budget. That is a property of the
509
- * measurement, not flakiness: run this block on an otherwise idle machine and parallelise the behavioural
510
- * block above. Loosening a budget to survive a parallel run would throw away the regression signal the
511
- * budget exists for.
512
- */
454
+ // Loose budgets that catch a reintroduced eager import or a per-save ratchet, not exact numbers. Absolute RSS is
455
+ // not parallel-safe: run this block alone on an idle machine (`-t "dev resource budgets"`), never loosen it.
513
456
  describe("dev resource budgets", () => {
514
457
  const BOOT_MS = 150_000;
515
458
  const WAIT_MS = 90_000;
516
- /**
517
- * Measured on this fixture at **260-516ms** per save. Set ~6× above that, like every budget here: it is
518
- * looking for a path that got slower by a factor — a boot build that stopped being avoidable, a worker
519
- * spawn that stopped being warm — not for jitter on a busy laptop. `apps/akan`, ~25× this fixture's
520
- * pages bundle, took ~1.5s per save when the plan started.
521
- */
459
+ // ~6x the measured 260-516ms per save: catches a path that got slower by a factor, not jitter.
522
460
  const SAVE_LATENCY_BUDGET_MS = 3_000;
523
461
  const budgetTest = (name: string, fn: () => Promise<void>): void => {
524
462
  if (integrationEnabled) test(name, fn, 300_000);
@@ -533,9 +471,7 @@ describe("dev resource budgets", () => {
533
471
  // A full minified browser-target build of every page, only reachable via `/__csr` and `?csr=true`.
534
472
  expect(host.logs.join("")).not.toMatch(/\[csr-build\] output ->/);
535
473
 
536
- // Mobile local dev points a device WebView at this URL, so it must serve HTML, not a 404. Wait
537
- // for the app to actually serve first: `backend ready` fires before the gateway routes to the
538
- // replica, and a too-early request 503s without reaching the router that arms CSR.
474
+ // `backend ready` fires before the gateway routes to the replica; an earlier request 503s without arming CSR.
539
475
  await harness.waitForHttpText("initial-client-marker", WAIT_MS);
540
476
  const armMark = host.markLog();
541
477
  const res = await fetch(`http://127.0.0.1:${port}/?csr=true`);
@@ -543,11 +479,7 @@ describe("dev resource budgets", () => {
543
479
  expect(await res.text()).toContain("<html");
544
480
  expect(host.logs.join("").slice(armMark)).toMatch(/csr-build ok on demand/);
545
481
 
546
- // Armed: from here on every save rebuilds CSR, which is what keeps a live mobile session working.
547
- //
548
- // `editUntilSeen` rather than a bare save, because the CSR build above emits a whole minified tree
549
- // into `.akan/artifact/csr` and a save landing in that window is dropped by Bun 100% of the time
550
- // (`local/optimize-resource/06-watcher-dropped-event.md`).
482
+ // `editUntilSeen`: Bun always drops a save landing in the CSR build's write burst.
551
483
  const { mark: resyncMark } = await harness.editUntilSeen(host, (attempt) =>
552
484
  harness.replaceText(
553
485
  "ui/ClientMarker.tsx",
@@ -561,8 +493,7 @@ describe("dev resource budgets", () => {
561
493
  budgetTest("bounds the rsc worker and the tree across repeated saves", async () => {
562
494
  const harness = await createHarness();
563
495
  const host = await harness.startHost({
564
- // Recycle on the second reload so this needs a couple of saves rather than the ten a
565
- // default-threshold run would take, and take the burst-coalescing floor out of the equation.
496
+ // Recycle on the second reload instead of the default tenth, with no burst-coalescing floor.
566
497
  timeoutMs: BOOT_MS,
567
498
  env: { AKAN_RSC_WORKER_MAX_RELOADS: "1", AKAN_RSC_WORKER_MIN_RECYCLE_INTERVAL_MS: "1" },
568
499
  });
@@ -571,22 +502,13 @@ describe("dev resource budgets", () => {
571
502
  const idleTotal = await DevStabilityHarness.processTreeRssBytes(host.proc.pid);
572
503
  const idleWithoutBuilder = await DevStabilityHarness.processTreeRssBytes(host.proc.pid, { excludeBuilder: true });
573
504
  const idleBuilder = await DevStabilityHarness.builderProcess(host.proc.pid);
574
- // Nothing should be building at idle, so the disposable worker must not be resident.
575
505
  expect(await DevStabilityHarness.buildWorkerProcess(host.proc.pid)).toBeNull();
576
- // Printed so a run that came close to its budget says so, instead of being indistinguishable from a
577
- // comfortable one — the same reason the recycle and idle-suspend guards report their numbers.
578
506
  console.info(
579
507
  `[budget-guard] idle tree ${Math.round(idleTotal / MB)}MB (builder ${Math.round((idleBuilder?.rssBytes ?? 0) / MB)}MB, rest ${Math.round(idleWithoutBuilder / MB)}MB)`,
580
508
  );
581
- // Split in two because the two halves have very different variance. The builder legitimately swings
582
- // from a ~130MB fresh floor to ~520MB once it has built a route (`Bun.build` arenas the RSS-ceiling
583
- // recycle is what bounds), so a tight total would flake. Measured 929-959MB total / ~414MB without
584
- // the builder; the old 1000MB total came from a phase-1 topology where the fixture measured ~670MB
585
- // and is only ~4% above what the current topology legitimately uses.
509
+ // Split: the builder alone swings ~130-520MB with `Bun.build` arenas, so a tight total would flake.
586
510
  expect(idleTotal).toBeLessThan(1_200 * MB);
587
- // This is the half that catches what the budget exists for: an eager import lands in the dev host or
588
- // the backend, not in the builder's arenas. The cheapest reintroduction is ~30MB and the devkit
589
- // barrel cycle was 236MB, so this bound still fails on the latter.
511
+ // An eager import lands here, in the dev host or backend, not in the builder's arenas.
590
512
  expect(idleWithoutBuilder).toBeLessThan(600 * MB);
591
513
 
592
514
  const start = host.markLog();
@@ -599,53 +521,39 @@ describe("dev resource budgets", () => {
599
521
  const afterSave = host.logs.join("").slice(mark);
600
522
  expect(afterSave).toMatch(/csr-rebundle skipped/);
601
523
  expect(afterSave).not.toMatch(/csr-rebundle ok/);
602
- // Waiting for the builder alone is not enough before the next iteration: the backend is still
603
- // applying the reload after that, and it writes into the watched tree too. `editUntilSeen` handles
604
- // the drop window itself, but these two waits are still what makes each iteration a whole
605
- // generation, which is what the RSS deltas below are measured across.
524
+ // The backend applies the reload after the builder finishes; wait for both so each save is a whole generation.
606
525
  await host.waitForLogSince(mark, /css-rebuild checked/, WAIT_MS);
607
526
  await host.waitForLogSince(mark, /\[hmr\] backend apply/, WAIT_MS);
608
527
  }
609
528
 
610
- // Each in-place reload re-imports the pages bundle under a fresh `?v=`, and Bun's ESM registry
611
- // never evicts — so without a recycle the worker grows for the life of the process.
529
+ // Each reload re-imports the pages bundle under a fresh `?v=`, and Bun's ESM registry never evicts.
612
530
  await host.waitForLogSince(start, /rolling recycle worker reason=pages-reload-accumulation/, WAIT_MS);
613
531
 
614
- // The dev host, gateway, replica and rsc worker must all stay flat across saves.
615
532
  const afterWithoutBuilder = await DevStabilityHarness.processTreeRssBytes(host.proc.pid, {
616
533
  excludeBuilder: true,
617
534
  });
618
535
  expect(afterWithoutBuilder - idleWithoutBuilder).toBeLessThan(120 * MB);
619
536
 
620
- // And so must the builder. It used to be excluded from this budget because `Bun.build` retains
621
- // native arenas no GC reclaims, which made it grow ~120MB per save on this fixture; every build
622
- // that scales per save now runs in a process that exits, so its memory goes back to the OS.
537
+ // Every per-save build runs in a worker that exits, so the builder must stay flat too.
623
538
  const afterBuilder = await DevStabilityHarness.builderProcess(host.proc.pid);
624
539
  expect(afterBuilder?.pid).toBe(idleBuilder?.pid);
625
540
  expect((afterBuilder?.rssBytes ?? 0) - (idleBuilder?.rssBytes ?? 0)).toBeLessThan(30 * MB);
626
- // The worker is transient: three generations built, and none of them is still around.
627
541
  expect(await DevStabilityHarness.buildWorkerProcess(host.proc.pid)).toBeNull();
628
542
  });
629
543
 
630
544
  budgetTest("recycles the builder at an unmeetable ceiling and keeps developing through it", async () => {
631
545
  const harness = await createHarness();
632
- // Deliberately *below* this fixture's post-boot builder. Moving every per-save build into a
633
- // disposable worker means the builder no longer grows into a ceiling, so a ceiling it is already
634
- // over is the only way left to drive the recycle path end to end — and it is also the case the
635
- // escape hatch exists for: an app whose boot floor simply does not fit under the limit.
546
+ // Deliberately below the post-boot builder: it no longer grows into a ceiling, so it must start over one.
636
547
  const host = await harness.startHost({ timeoutMs: BOOT_MS, env: { AKAN_BUILDER_MAX_RSS_MB: "200" } });
637
548
  const start = host.markLog();
638
549
  await harness.waitForHttpText("initial-client-marker", WAIT_MS);
639
550
 
640
- // One save is enough: the builder reports its rss as soon as the batch drains, and the host arms
641
- // the recycle from that report. The old pid comes from the log rather than from `ps`, so this does
642
- // not race the swap it is about to observe.
551
+ // One save arms the recycle; the old pid comes from the log, not `ps`, so it cannot race the swap.
643
552
  const { mark: firstSave } = await harness.editUntilSeen(host, (attempt) =>
644
553
  harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-1-${attempt}`),
645
554
  );
646
555
  await host.waitForLogSince(firstSave, /pages-rebundle ok/, WAIT_MS);
647
556
 
648
- // The host decides, the builder drains rather than being killed, and the replacement comes up.
649
557
  const recycleLog = await host.waitForLogSince(
650
558
  start,
651
559
  /recycling builder pid=(\d+) \((rss=\d+MiB>=200MiB after \d+ build\(s\))\)/,
@@ -654,19 +562,14 @@ describe("dev resource budgets", () => {
654
562
  await host.waitForLogSince(start, /exiting for recycle/, WAIT_MS);
655
563
  await host.waitForLogSince(start, /builder spawned pid=\d+ .*restart=1/, WAIT_MS);
656
564
  await host.waitForLogSince(start, /builder ready after restart/, WAIT_MS);
657
- // The backend read `base-artifact.json` once at boot, so the replacement has to re-announce what
658
- // it booted with or the backend keeps serving the artifact of the builder that just exited.
565
+ // The backend read `base-artifact.json` once at boot, so the replacement must re-announce its boot state.
659
566
  await host.waitForLogSince(start, /announced boot state after recycle/, WAIT_MS);
660
567
 
661
568
  const recycled = await DevStabilityHarness.builderProcess(host.proc.pid);
662
569
  expect(recycled).not.toBeNull();
663
570
  expect(String(recycled?.pid)).not.toBe(recycleLog[1]);
664
571
 
665
- // And the dev server is still a dev server: the replacement watches, rebuilds and serves.
666
- //
667
- // Waiting for readiness above is load-bearing, not padding. The watcher is installed at the end of
668
- // the boot build, so a save during the recycle is seen by neither builder — this test lost one
669
- // exactly that way.
572
+ // The readiness waits above are load-bearing: a save during the recycle is seen by neither builder.
670
573
  const postRecycle = await harness.editUntilSeen(host, (attempt) =>
671
574
  harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-after-recycle-${attempt}`),
672
575
  );
@@ -676,9 +579,7 @@ describe("dev resource budgets", () => {
676
579
  );
677
580
  await harness.waitForHttpText("marker-after-recycle", WAIT_MS);
678
581
 
679
- // This fixture's builder boots well under the ceiling and blows past it on its first build, which
680
- // is the shape of every app the ceiling is derived for — a 1.2GB sandbox gives the builder ~420MB
681
- // and a single route build costs ~247MB. So the host says the ceiling is tight...
582
+ // A tight ceiling is reported, and still enforced (recycles are throttled), never disabled for the session.
682
583
  for (let i = 1; i <= 3; i++) {
683
584
  const { mark } = await harness.editUntilSeen(host, (attempt) =>
684
585
  harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-settled-${i}-${attempt}`),
@@ -686,80 +587,53 @@ describe("dev resource budgets", () => {
686
587
  await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
687
588
  }
688
589
  await host.waitForLogSince(start, /ceiling costs about one boot build per interval/, WAIT_MS);
689
- // ...and goes on enforcing it. This used to disable the ceiling for the rest of the session on the
690
- // same evidence, which on a small sandbox means nothing bounds the builder from the first page load
691
- // onwards. Recycling is throttled to one per interval; that throttle is the answer to the cost, not
692
- // dropping the bound.
693
590
  expect(host.logs.join("").slice(start)).not.toMatch(/no longer enforcing it this session/);
694
591
  });
695
592
 
696
- // The guard the memory work did not have. Every budget above asserts RSS; none of them asserts that a
697
- // page still renders while the machinery those budgets require is mid-swap — and that gap is where two
698
- // shipped defects lived (`local/optimize-resource/19-shutdown-and-restart-races.md`).
699
593
  budgetTest("serves a page requested while the builder is being replaced", async () => {
700
594
  const harness = await createHarness();
701
- // Same unmeetable ceiling as the guard above, for the same reason: a builder that no longer grows
702
- // into a ceiling has to start under one for the recycle path to run end to end.
703
595
  const host = await harness.startHost({ timeoutMs: BOOT_MS, env: { AKAN_BUILDER_MAX_RSS_MB: "200" } });
704
596
  const port = await harness.resolvePort();
705
597
  await harness.waitForHttpText("initial-client-marker", WAIT_MS);
706
598
 
707
599
  const start = host.markLog();
708
- // One save does both halves of the setup: it drops the route's client entries, so the next request
709
- // has to ask the builder again rather than being served from cache, and it makes the builder report
710
- // an rss over the ceiling, which is what arms the recycle.
600
+ // One save both evicts the route's cached client entries and arms the recycle.
711
601
  const { mark } = await harness.editUntilSeen(host, (attempt) =>
712
602
  harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-through-recycle-${attempt}`),
713
603
  );
714
604
  await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS);
715
605
 
716
- // A builder that has been asked to drain is still alive and refuses everything new, so this request
717
- // has to be held for the replacement exactly like one that arrives after the process is gone. Timing
718
- // decides which of the two windows it actually lands in — the drain of an idle builder is short, and
719
- // the log is polled — so what is asserted is the outcome both windows must produce. The drain itself
720
- // is pinned deterministically one layer down, in `incrementalBuilder.host.test.ts`.
606
+ // Timing decides whether this lands in the drain or after the exit, so assert what both must produce.
721
607
  await host.waitForLogSince(start, /recycling builder pid=\d+/, WAIT_MS);
722
- // Not awaited here: a held request only returns once the replacement is up, and waiting for it would
723
- // put every assertion below on the far side of the window they are about.
608
+ // Not awaited: a held request returns only once the replacement is up.
724
609
  const drainRequest = fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(WAIT_MS) }).then(
725
610
  async (response) => ({ status: response.status, html: await response.text() }),
726
611
  );
727
612
 
728
- // Wait until the replacement exists but is still doing its boot build. From here it cannot answer
729
- // anything for seconds — which is exactly the window a developer's reload lands in, and the point of
730
- // anchoring on the spawn rather than on the exit: the gap after it is wide and known, not a race.
613
+ // Anchored on the spawn: the replacement's boot build leaves a wide, known window rather than a race.
731
614
  await host.waitForLogSince(start, /exiting for recycle/, WAIT_MS);
732
615
  await host.waitForLogSince(start, /builder spawned pid=\d+ .*restart=1/, WAIT_MS);
733
616
 
734
617
  const holdMark = host.markLog();
735
618
  const startedAtMono = performance.now();
736
- // The second route, never requested before, so this one cannot be answered from the route cache the
737
- // request above just populated — it has to reach a builder that is not there yet.
619
+ // Never requested before, so no route cache can answer it; it has to reach a builder that is not there yet.
738
620
  const res = await fetch(`http://127.0.0.1:${port}/second`, { signal: AbortSignal.timeout(WAIT_MS) });
739
621
  const html = await res.text();
740
622
  const heldMs = Math.round(performance.now() - startedAtMono);
741
623
 
742
- // What the developer sees, asserted first because it is the whole point: with the hold removed this
743
- // same request is answered **500** by the dev error page, and nothing retries on its own — the tab
744
- // stays broken until it is reloaded by hand. Measured both ways before this guard was committed.
624
+ // Without the hold this is a 500 from the dev error page, which never retries on its own.
745
625
  expect(res.status).toBe(200);
746
626
  expect(html).toContain("marker-through-recycle");
747
627
  expect(html).not.toContain("reload after the builder is ready");
748
- // And this is what stops the test passing having tested nothing: a request that arrives after the
749
- // replacement is already ready never reaches the path under test, and everything above holds anyway.
628
+ // Proves the request reached the hold, rather than arriving after the replacement was already ready.
750
629
  expect(host.logs.join("").slice(holdMark)).toMatch(/holding build-route until the builder is ready/);
751
630
 
752
- // The one fired at the drain, whichever side of the exit it landed on.
753
631
  const drained = await drainRequest;
754
632
  expect(drained.status).toBe(200);
755
633
  expect(drained.html).toContain("marker-through-recycle");
756
634
  console.info(`[restart-guard] page held ${heldMs}ms across the recycle, then rendered`);
757
635
  });
758
636
 
759
- // The other half of a resource budget, and the half this plan spent: every megabyte above was bought
760
- // with latency — a worker process spawned per save, a boot build per recycle and per wake, a hold
761
- // window in front of requests that land in one. Nothing measured what that cost, so a change that
762
- // traded another second per save for another 50MB would have passed every guard in this block.
763
637
  budgetTest("keeps a save's round trip inside its budget", async () => {
764
638
  const harness = await createHarness();
765
639
  const host = await harness.startHost({ timeoutMs: BOOT_MS });
@@ -768,45 +642,37 @@ describe("dev resource budgets", () => {
768
642
  const samples: number[] = [];
769
643
  for (let i = 1; i <= 3; i++) {
770
644
  let savedAtMono = 0;
771
- // Timed from inside the mutate callback so a retried save — Bun drops watcher events, which is why
772
- // `editUntilSeen` exists — is measured from the attempt that actually landed, not from the first.
645
+ // Timed inside the mutate callback, so a retried save is measured from the attempt that landed.
773
646
  const { mark } = await harness.editUntilSeen(host, async (attempt) => {
774
647
  await harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-latency-${i}-${attempt}`);
775
648
  savedAtMono = performance.now();
776
649
  });
777
- // The moment the running dev server is serving the new code, which is everything the developer is
778
- // waiting for. Deliberately not the browser's refresh message: that one is only published when a
779
- // client is connected, so measuring it would measure a WebSocket probe's reconnects as well.
650
+ // Not the refresh message: that is published only while a client is connected.
780
651
  await host.waitForLogSince(mark, /\[hmr\] backend apply/, WAIT_MS);
781
652
  samples.push(performance.now() - savedAtMono);
782
653
  }
783
654
 
784
655
  const rounded = samples.map((ms) => Math.round(ms));
785
656
  console.info(`[latency-guard] save -> new code live ${rounded.join("ms, ")}ms`);
786
- // Per save, not summed: the interesting regression is one save getting slower, and the median would
787
- // hide a first save that pays for something the rest do not.
657
+ // The worst save, not a median, which would hide a first save that pays for something the rest do not.
788
658
  expect(Math.max(...rounded)).toBeLessThan(SAVE_LATENCY_BUDGET_MS);
789
659
  });
790
660
 
791
661
  budgetTest("suspends the builder when the dev server goes idle and wakes it on the next edit", async () => {
792
662
  const harness = await createHarness();
793
- // 3s stands in for the 5min default: the machinery is the same, and the guard needs the dev server
794
- // to actually reach idle inside a test.
663
+ // 3s stands in for the 5min default so the dev server reaches idle inside a test.
795
664
  const host = await harness.startHost({ timeoutMs: BOOT_MS, env: { AKAN_DEV_IDLE_SUSPEND_MS: "3000" } });
796
665
  const start = host.markLog();
797
666
  await harness.waitForHttpText("initial-client-marker", WAIT_MS);
798
667
 
799
- // Nothing periodic may keep the dev server "busy": if the builder reported metrics on a timer, or
800
- // the backend wrote a watched file, this would never fire.
668
+ // Never fires if anything periodic (timed metrics, a watched-file write) keeps the dev server busy.
801
669
  await host.waitForLogSince(start, /\[idle-suspend\] no build activity for \d+s; released the builder/, WAIT_MS);
802
670
  const suspendedBuilder = await DevStabilityHarness.builderProcess(host.proc.pid);
803
671
  expect(suspendedBuilder).toBeNull();
804
672
  // Only build capacity suspends — the backend keeps serving the preview URL.
805
673
  await harness.waitForHttpText("initial-client-marker", WAIT_MS);
806
674
 
807
- // The edit that has to wake it. A suspended host has just installed a fresh watcher over a tree the
808
- // suspend itself churned, so this is squarely inside Bun's drop window — it is what failed here in a
809
- // 3-way parallel run, on 60s of empty log output.
675
+ // A fresh watcher over a tree the suspend just churned: squarely inside Bun's drop window.
810
676
  const { mark } = await harness.editUntilSeen(
811
677
  host,
812
678
  (attempt) => harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-after-wake-${attempt}`),
@@ -821,7 +687,6 @@ describe("dev resource budgets", () => {
821
687
  await harness.waitForHttpText("marker-after-wake", WAIT_MS);
822
688
  console.info(`[idle-suspend-guard] woke in ${awake[1]}ms; builder back at pid=${wokenBuilder?.pid}`);
823
689
 
824
- // And it is a normal dev server again afterwards.
825
690
  const postWake = await harness.editUntilSeen(host, (attempt) =>
826
691
  harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-postwake-${attempt}`),
827
692
  );
@@ -838,8 +703,7 @@ describe("dev resource budgets", () => {
838
703
  await host.waitForLogSince(start, /\[idle-suspend\] .*released the builder/, WAIT_MS);
839
704
  expect(await DevStabilityHarness.builderProcess(host.proc.pid)).toBeNull();
840
705
 
841
- // A browser asking for something the backend cannot serve on its own is the other wake trigger, and
842
- // the request must not be answered with "builder is stopped" the way a dead builder's would be.
706
+ // A request only the builder can serve also wakes it, and must not be answered "builder is stopped".
843
707
  const mark = host.markLog();
844
708
  const port = await harness.resolvePort();
845
709
  const status = await fetch(`http://127.0.0.1:${port}/__csr`)
@@ -847,19 +711,13 @@ describe("dev resource budgets", () => {
847
711
  .catch(() => 0);
848
712
 
849
713
  await host.waitForLogSince(mark, /\[idle-suspend\] waking \(build-csr arrived while suspended\)/, WAIT_MS);
850
- // `[builder]`, not `[idle-suspend]`: the same queue now also holds requests across a builder restart,
851
- // so the replay says what it did rather than which of the two reasons put the request there. The wake
852
- // line above is what distinguishes them, and it is asserted first for that reason.
714
+ // `[builder]`: the same queue holds requests across a restart too; the wake line above tells them apart.
853
715
  await host.waitForLogSince(mark, /\[builder\] replaying 1 request\(s\) held while the builder was away/, WAIT_MS);
854
716
  expect(status).toBe(200);
855
717
  expect(await DevStabilityHarness.builderProcess(host.proc.pid)).not.toBeNull();
856
718
  });
857
719
 
858
- // The gap on the other side of the same window: requests held across it are answered, but for a while
859
- // nothing was *watching* across it. The suspend stops its watcher before the replacement builder has
860
- // primed its index, and the replacement primes from the disk it finds — so a save in between is
861
- // baseline to it, reported by nobody, and the backend goes on running the code it replaced. Phase 2
862
- // made this window routine by recycling the builder on every rss ceiling crossing.
720
+ // Nothing watches between the suspend stopping its watcher and the replacement priming its index from disk.
863
721
  budgetTest("restarts the backend for a save that lands while the builder is away", async () => {
864
722
  const harness = await createHarness();
865
723
  const host = await harness.startHost({ timeoutMs: BOOT_MS, env: { AKAN_DEV_IDLE_SUSPEND_MS: "3000" } });
@@ -869,13 +727,9 @@ describe("dev resource budgets", () => {
869
727
 
870
728
  const mark = host.markLog();
871
729
  const port = await harness.resolvePort();
872
- // This request is the clock: it wakes the dev server and is answered only once the replacement
873
- // builder is ready, so a write issued while it is in flight lands squarely inside the gap. Its own
874
- // status is not asserted here — the write below restarts the backend, which drops the connection.
730
+ // The clock: in flight until the replacement is ready; its status is moot once the backend restarts.
875
731
  const request = fetch(`http://127.0.0.1:${port}/__csr`).catch(() => null);
876
- // A service the backend imports, rather than this fixture's `srvkit/backendMarker.ts`: that marker is
877
- // orphaned once `akan start` regenerates `server.ts`, so it is not in the backend graph at all and an
878
- // edit to it restarts the backend by path role instead.
732
+ // Not `srvkit/backendMarker.ts`: the regenerated `server.ts` orphans it, so it restarts by path role only.
879
733
  await harness.writeFile(
880
734
  "lib/_fixture/fixture.service.ts",
881
735
  `import { serve } from "akanjs/service";
@@ -888,7 +742,6 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
888
742
 
889
743
  await host.waitForLogSince(mark, /\[builder-gap\] 1 backend file\(s\) changed while the builder was away/, WAIT_MS);
890
744
  await host.waitForLogSince(mark, /\[backend-reload\]/, WAIT_MS);
891
- // And back to a working dev server, rather than one stuck restarting.
892
745
  await host.waitForLogSince(mark, /backend ready pid=(\d+)|AkanApp gateway is running on port/, WAIT_MS);
893
746
  await harness.waitForHttpText("initial-client-marker", WAIT_MS);
894
747
  });