@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20

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 (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. package/workflow/primitive.ts +0 -59
@@ -28,6 +28,13 @@ import {
28
28
  shouldWarnBuilderRssCeilingTight,
29
29
  } from "./devHostPolicy";
30
30
 
31
+ const status = (phase: BuildPhase, generation: number, ok: boolean): DevBuildStatus => ({
32
+ generation,
33
+ phase,
34
+ ok,
35
+ files: [],
36
+ });
37
+
31
38
  const invalidateWithActions = (actions: DevChangeAction[]): Extract<BuilderMessage, { type: "invalidate" }> => ({
32
39
  type: "invalidate",
33
40
  kinds: ["code"],
@@ -158,8 +165,6 @@ describe("holding requests for a returning builder", () => {
158
165
  });
159
166
 
160
167
  test("holds through the drain too, not only after the process is gone", () => {
161
- // The window this decision originally missed: the builder is still alive and refusing, which is
162
- // the same gap as a restart from anyone waiting on a page.
163
168
  expect(shouldHoldForReturningBuilder({ status: "recycling", heldCount: 0 })).toBe(true);
164
169
  });
165
170
 
@@ -196,8 +201,6 @@ describe("builder rss recycle", () => {
196
201
  expect(decide({ ceilingBytes: null })).toBe("unbounded");
197
202
  });
198
203
 
199
- // Rebooting on a generation whose build failed strands the dev server: the replacement hits the
200
- // same compile error and exits before builder-ready.
201
204
  test("defers while the current generation has a failing build", () => {
202
205
  expect(decide({ buildFailed: true })).toBe("build-failed");
203
206
  });
@@ -208,34 +211,25 @@ describe("builder rss recycle", () => {
208
211
  expect(decide({ msSinceLastRecycle: 5_000, minIntervalMs: 1_000 })).toBe("recycle");
209
212
  });
210
213
 
211
- // Says so, and keeps enforcing: a page load is two route builds, so an app whose builds sit over the
212
- // ceiling reaches this on its first navigation — which is normal work, not a reason to drop the only
213
- // bound the builder has.
214
214
  test("mentions a tight ceiling rather than acting on it", () => {
215
215
  expect(shouldWarnBuilderRssCeilingTight(1)).toBe(false);
216
216
  expect(shouldWarnBuilderRssCeilingTight(2)).toBe(true);
217
217
  expect(shouldWarnBuilderRssCeilingTight(1, 1)).toBe(true);
218
218
  });
219
219
 
220
- // The one case recycling cannot fix, measured on the replacement before it has built anything: every
221
- // future replacement lands on the same floor, so the loop would only ever cost boot builds.
222
220
  test("gives up only when a fresh builder is already over the ceiling", () => {
223
221
  expect(isRssCeilingUnreachable(ceiling + 1, ceiling)).toBe(true);
224
222
  expect(isRssCeilingUnreachable(ceiling - 1, ceiling)).toBe(false);
225
- // An unreadable rss is no information, and no ceiling is nothing to be unreachable.
226
223
  expect(isRssCeilingUnreachable(null, ceiling)).toBe(false);
227
224
  expect(isRssCeilingUnreachable(ceiling + 1, null)).toBe(false);
228
225
  });
229
226
 
230
- // Measured on Linux: the builder peaked at 522MiB and settled at 214MiB with no help, so a 400MiB
231
- // ceiling recycled a process that was already back under it. The armed sample is always the peak.
232
227
  describe("settle check before committing", () => {
233
228
  test("waits when the builder is only modestly over the ceiling", () => {
234
229
  expect(decideBuilderRssSettle({ rssBytes: 522, ceilingBytes: 400 })).toBe("wait-and-recheck");
235
230
  expect(decideBuilderRssSettle({ rssBytes: 401, ceilingBytes: 400 })).toBe("wait-and-recheck");
236
231
  });
237
232
 
238
- // No purge is going to rescue a builder this far over, so waiting only delays the inevitable.
239
233
  test("recycles immediately once far enough past the ceiling", () => {
240
234
  expect(decideBuilderRssSettle({ rssBytes: 600, ceilingBytes: 400 })).toBe("recycle-now");
241
235
  expect(decideBuilderRssSettle({ rssBytes: 900, ceilingBytes: 400 })).toBe("recycle-now");
@@ -268,7 +262,6 @@ describe("dev idle suspend", () => {
268
262
  expect(decide({ restartPending: true })).toBe("restart-pending");
269
263
  });
270
264
 
271
- // A wake would boot straight back into the same compile error, and the developer is mid-fix anyway.
272
265
  test("never suspends on a red build", () => {
273
266
  expect(decide({ buildFailed: true })).toBe("build-failed");
274
267
  });
@@ -289,14 +282,13 @@ describe("dev idle suspend", () => {
289
282
  });
290
283
 
291
284
  test("blocks a suspend on a failure in any phase, not just the newest generation", () => {
292
- const status = (phase: BuildPhase, ok: boolean): DevBuildStatus => ({ generation: 1, phase, ok, files: [] });
293
285
  expect(hasAnyBuildFailure(new Map())).toBe(false);
294
- expect(hasAnyBuildFailure(new Map([["scan", status("scan", true)]]))).toBe(false);
286
+ expect(hasAnyBuildFailure(new Map([["scan", status("scan", 1, true)]]))).toBe(false);
295
287
  expect(
296
288
  hasAnyBuildFailure(
297
289
  new Map([
298
- ["scan", status("scan", true)],
299
- ["pages", status("pages", false)],
290
+ ["scan", status("scan", 1, true)],
291
+ ["pages", status("pages", 1, false)],
300
292
  ]),
301
293
  ),
302
294
  ).toBe(true);
@@ -332,8 +324,6 @@ describe("recycled builder state announcements", () => {
332
324
  },
333
325
  });
334
326
 
335
- // Both identities are content hashes, so a recycle with no concurrent edit reproduces them exactly
336
- // and must not reload the backend — that would refresh every browser on a memory recycle.
337
327
  test("suppresses an unchanged pages announcement and relays a moved one", () => {
338
328
  expect(shouldRelayRecycledFrontendState(pages("/a/pages-abc.js"), pages("/a/pages-abc.js"))).toBe(false);
339
329
  expect(shouldRelayRecycledFrontendState(pages("/a/pages-abc.js"), pages("/a/pages-def.js"))).toBe(true);
@@ -355,13 +345,6 @@ describe("recycled builder state announcements", () => {
355
345
  });
356
346
 
357
347
  describe("build status helpers", () => {
358
- const status = (phase: DevBuildStatus["phase"], generation: number, ok: boolean): DevBuildStatus => ({
359
- generation,
360
- phase,
361
- ok,
362
- files: [],
363
- });
364
-
365
348
  test("tracks recovery by phase without unrelated phases masking failures", () => {
366
349
  const previousByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
367
350
  ["pages", status("pages", 10, false)],
@@ -441,13 +424,6 @@ describe("backend recovery abandonment", () => {
441
424
  });
442
425
 
443
426
  describe("hasBuildFailureForGeneration", () => {
444
- const status = (phase: DevBuildStatus["phase"], generation: number, ok: boolean): DevBuildStatus => ({
445
- generation,
446
- phase,
447
- ok,
448
- files: [],
449
- });
450
-
451
427
  test("detects a failing phase recorded for the same generation", () => {
452
428
  const statusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
453
429
  ["csr", status("csr", 3, false)],
@@ -11,11 +11,7 @@ import type { IncrementalBuilderStatus } from "../incrementalBuilder";
11
11
 
12
12
  const BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
13
13
 
14
- /**
15
- * How many requests may wait for a builder that is coming back. Generous — a page load asks for
16
- * several routes — but finite, so a builder that never returns cannot grow this without bound. Past
17
- * it, requests are failed as they were before, which is the behaviour this limit falls back to.
18
- */
14
+ // A page load asks for several routes, but a builder that never returns must not grow the hold queue unbounded.
19
15
  const HELD_BUILDER_REQUEST_LIMIT = 64;
20
16
 
21
17
  export const BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30_000;
@@ -25,8 +21,6 @@ const BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
25
21
  // Far enough above the ceiling that no purge would rescue it; recycle without waiting.
26
22
  const BUILDER_RSS_HARD_MULTIPLE = 1.5;
27
23
 
28
- // A sandbox between user turns pays for a watcher that is watching nothing change. Suspending build
29
- // capacity after this long returns the builder's residency until the next edit or route request.
30
24
  const DEV_IDLE_SUSPEND_MS = 300_000;
31
25
 
32
26
  // A wake that immediately suspends again would flap around whatever woke it.
@@ -34,7 +28,6 @@ const DEV_IDLE_MIN_UPTIME_MS = 30_000;
34
28
 
35
29
  export const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
36
30
 
37
- /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
38
31
  const SERVER_SUFFIXES = [".service.ts", ".document.ts"];
39
32
 
40
33
  const SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
@@ -53,10 +46,6 @@ export const shouldRestartBackendByDevPlan = (
53
46
  export const shouldRestartBuilderByDevPlan = (message: Extract<BuilderMessage, { type: "invalidate" }>): boolean =>
54
47
  message.devPlan?.actions.includes("restart-builder") ?? false;
55
48
 
56
- /**
57
- * A backend that keeps dying isn't going to heal by retrying the same code; after this many
58
- * consecutive attempts the host idles and the next server-side edit triggers a fresh restart.
59
- */
60
49
  export const shouldAbandonBackendRecovery = (attempts: number, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS): boolean =>
61
50
  attempts >= maxAttempts;
62
51
 
@@ -69,11 +58,7 @@ export const shouldRestartDevHostByDevPlan = (message: Extract<BuilderMessage, {
69
58
 
70
59
  export type BackendLifecycleState = "starting" | "ready" | "restart-pending" | "stopping" | "recovering" | "stopped";
71
60
 
72
- /**
73
- * What a dev host tells a supervising process about itself. Reported rather than scraped: the parent
74
- * gets one pipe of interleaved child output, in which "this app is up" is indistinguishable from a line
75
- * that merely mentions it.
76
- */
61
+ /** Reported, not scraped: in interleaved child output "this app is up" reads like any line mentioning it. */
77
62
  export type DevHostState = "starting" | "ready" | "restarting" | "recovering" | "suspended" | "failed" | "stopped";
78
63
 
79
64
  export interface DevHostEvent {
@@ -91,11 +76,7 @@ const devHostStateByBackendState = {
91
76
  stopped: "stopped",
92
77
  } as const satisfies { [key in BackendLifecycleState]: DevHostState };
93
78
 
94
- /**
95
- * The backend's own lifecycle collapsed to what a supervisor can act on. `stopped` is the one state
96
- * that is ambiguous on its own — a planned shutdown and a crash loop that ran out of retries both land
97
- * there — so the give-up flag is what separates "gone" from "broken".
98
- */
79
+ /** `stopped` is both a planned shutdown and an exhausted crash loop; `gaveUp` maps the latter to `failed`. */
99
80
  export const devHostStateOf = (state: BackendLifecycleState, gaveUp: boolean): DevHostState =>
100
81
  state === "stopped" && gaveUp ? "failed" : devHostStateByBackendState[state];
101
82
 
@@ -192,12 +173,7 @@ export const shouldReplaceLastGoodMessage = (
192
173
  export const shouldQueueBuildStatusReplay = (backendReady: boolean, pendingReplayCount: number): boolean =>
193
174
  !backendReady || pendingReplayCount > 0;
194
175
 
195
- /**
196
- * Recycling the builder/backend on a generation whose build already failed is guaranteed to strand
197
- * the dev server: the rebooted builder hits the same compile error and exits before builder-ready.
198
- * Failing phase statuses for a generation arrive over IPC before that generation's invalidate, so
199
- * the host can check them here and defer the recycle until a healthy batch lands.
200
- */
176
+ /** Failing statuses arrive before their generation's invalidate; recycling on one strands the dev server. */
201
177
  export const hasBuildFailureForGeneration = (
202
178
  statusByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
203
179
  generation: number | undefined,
@@ -211,15 +187,7 @@ export const hasBuildFailureForGeneration = (
211
187
 
212
188
  export type BuilderRssRecycleDecision = "unbounded" | "below-ceiling" | "build-failed" | "too-soon" | "recycle";
213
189
 
214
- /**
215
- * Whether an over-ceiling builder should be replaced now.
216
- *
217
- * `Bun.build` never returns its native arenas, so the builder's RSS only comes back when the process
218
- * exits. Recycling it is therefore the only bound available — but it costs a boot build, so the two
219
- * cases where a recycle cannot help are excluded: a generation whose build already failed (the
220
- * replacement would hit the same compile error), and a recycle so soon after the last one that the
221
- * ceiling is evidently unreachable for this app.
222
- */
190
+ /** `Bun.build` never returns its native arenas, so recycling the builder is the only bound on its RSS. */
223
191
  export const decideBuilderRssRecycle = ({
224
192
  rssBytes,
225
193
  ceilingBytes,
@@ -242,11 +210,6 @@ export const decideBuilderRssRecycle = ({
242
210
 
243
211
  export type BuilderRssSettleDecision = "recycle-now" | "wait-and-recheck";
244
212
 
245
- /**
246
- * Whether an armed recycle should wait out the allocator's purge window before committing. A builder
247
- * far enough over the ceiling is not going to be rescued by a purge, so waiting there only delays a
248
- * recycle that has to happen.
249
- */
250
213
  export const decideBuilderRssSettle = ({
251
214
  rssBytes,
252
215
  ceilingBytes,
@@ -267,15 +230,7 @@ export type IdleSuspendDecision =
267
230
  | "too-soon"
268
231
  | "suspend";
269
232
 
270
- /**
271
- * Whether the dev host may drop its build capacity now. Every "no" here is a case where suspending
272
- * would either lose work or produce a wake that immediately re-suspends:
273
- *
274
- * - a red build means the developer is mid-fix and about to save again, and a wake would boot straight
275
- * back into the same error via the degraded-boot path
276
- * - a pending restart/recovery already has its own plan for the builder
277
- * - `too-soon` keeps a wake from flapping around whatever triggered it
278
- */
233
+ /** A red build means a save is imminent, and a wake would boot straight back into the same error. */
279
234
  export const decideIdleSuspend = ({
280
235
  enabled,
281
236
  suspended,
@@ -313,28 +268,13 @@ export const resolveIdleSuspendMs = (raw: string | undefined): number | null =>
313
268
  return Math.round(parsed);
314
269
  };
315
270
 
316
- /** Any red phase blocks a suspend, unlike the rss recycle which only cares about one generation. */
317
271
  export const hasAnyBuildFailure = (statusByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>): boolean =>
318
272
  [...statusByPhase.values()].some((status) => !status.ok);
319
273
 
320
- /**
321
- * A config change while suspended cannot be applied by restarting the builder alone — the dev host
322
- * itself has to re-read the config, which is the same path an ordinary config save takes.
323
- */
324
274
  export const shouldRefreshConfigOnIdleWake = (batch: ChangeBatch | null): boolean =>
325
275
  !!batch && batch.kinds.has("config");
326
276
 
327
- /**
328
- * Whether a request that arrived while the builder was away should wait for the one coming back.
329
- *
330
- * A recycle or a crash-restart is a gap, not a failure — the request that lands in it is the page a
331
- * developer is waiting on. A stopped builder is a different thing: nothing is bringing it back, so
332
- * waiting would only delay the error.
333
- *
334
- * `recycling` is in here for the same reason as `restarting`, and was the hole this decision shipped
335
- * with: a draining builder is still alive, so the request reached it and came back refused while the
336
- * host still thought there was nothing to wait for.
337
- */
277
+ // `recycling` too: a draining builder is alive and refuses; nothing brings a `stopped` one back, so it fails fast.
338
278
  const RETURNING_BUILDER_STATUSES = new Set<IncrementalBuilderStatus>(["starting", "recycling", "restarting"]);
339
279
  export const shouldHoldForReturningBuilder = ({
340
280
  status,
@@ -346,37 +286,17 @@ export const shouldHoldForReturningBuilder = ({
346
286
  limit?: number;
347
287
  }): boolean => RETURNING_BUILDER_STATUSES.has(status) && heldCount < limit;
348
288
 
349
- /**
350
- * Whether a builder that is over the ceiling again this soon after being replaced is worth saying so
351
- * about, once. Not a reason to stop enforcing the ceiling: the minimum interval already bounds what
352
- * this costs at one recycle per interval, and dropping the bound is how a container gets OOM-killed.
353
- *
354
- * This used to disable the ceiling for the session, on a count that a single page load reaches — two
355
- * route builds, two reports, both inside the interval. That is normal work on any app whose builds sit
356
- * above the ceiling, which is the same app the ceiling was derived for.
357
- */
289
+ /** Warn only, never stop enforcing: dropping the ceiling is how a container gets OOM-killed. */
358
290
  export const shouldWarnBuilderRssCeilingTight = (
359
291
  reportsSinceRecycle: number,
360
292
  limit = BUILDER_TIGHT_RSS_REPORT_LIMIT,
361
293
  ): boolean => reportsSinceRecycle >= limit;
362
294
 
363
- /**
364
- * Whether recycling can ever bring this builder under the ceiling.
365
- *
366
- * Measured on a replacement the moment it is ready, before it has built anything on demand: that is
367
- * the floor every future replacement lands on, so a floor already over the ceiling is the one case
368
- * where the recycle loop is pure cost. It is also the case the escape hatch was always described as
369
- * being for — the previous rule inferred it from report timing and caught ordinary work instead.
370
- */
295
+ /** Takes a fresh replacement's RSS: the floor every future replacement lands on. */
371
296
  export const isRssCeilingUnreachable = (freshRssBytes: number | null, ceilingBytes: number | null): boolean =>
372
297
  freshRssBytes !== null && ceilingBytes !== null && freshRssBytes >= ceilingBytes;
373
298
 
374
- /**
375
- * Whether a recycled builder's re-announced boot artifact actually differs from what the backend
376
- * already has. Both payload identities are content hashes — `pages-[hash].js` and
377
- * `<name>-[hash].css` — so an unchanged recycle produces identical ones and needs no reload. Only a
378
- * save that raced the recycle moves them, and that is the case worth pushing.
379
- */
299
+ /** Both identities are content hashes (`pages-[hash].js`, `<name>-[hash].css`), unchanged by a clean recycle. */
380
300
  export const shouldRelayRecycledFrontendState = (
381
301
  current:
382
302
  | Extract<BuilderMessage, { type: "pages-updated" }>
@@ -409,7 +329,6 @@ const mergeDevPlans = (current?: DevChangePlan, next?: DevChangePlan): DevChange
409
329
  };
410
330
  };
411
331
 
412
- /** A deferred recycle accumulates every batch it skipped so the eventual restart covers them all. */
413
332
  export const mergeInvalidateMessages = (
414
333
  current: Extract<BuilderMessage, { type: "invalidate" }>,
415
334
  next: Extract<BuilderMessage, { type: "invalidate" }>,
@@ -429,14 +348,9 @@ export const buildStatusReplaySequence = (
429
348
  latestByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
430
349
  ): DevBuildStatus[] => [...pendingReplay, ...latestByPhase.values()];
431
350
 
432
- /** `(mtimeMs, size)` per file — what a save moves, and what a rebuild of identical content does not. */
351
+ /** `mtimeMs:size` per file. */
433
352
  export type SourceFingerprints = ReadonlyMap<string, string>;
434
353
 
435
- /**
436
- * Which of the files in `before` are no longer stamped the way they were.
437
- *
438
- * Only files present in `before` are compared. The question this answers is which *running* code went
439
- * stale while nothing was watching, and a file that did not exist then is not running anywhere.
440
- */
354
+ /** Only files in `before`: a file that did not exist then is not running anywhere. */
441
355
  export const filesChangedSince = (before: SourceFingerprints, after: SourceFingerprints): string[] =>
442
356
  [...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
@@ -33,6 +33,22 @@ const baseDevEnv = {
33
33
  workspaceRoot: "/workspace",
34
34
  };
35
35
 
36
+ const loadExtAppConfig = async (tmpPrefix: string, appConfig: string, libConfig: string) => {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), tmpPrefix));
38
+ try {
39
+ fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
40
+ fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
41
+ fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), appConfig);
42
+ fs.writeFileSync(path.join(root, "libs/extlib/akan.config.ts"), libConfig);
43
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
44
+ fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
45
+ const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
46
+ return await AppExecutor.from(workspace, "extapp").getConfig();
47
+ } finally {
48
+ fs.rmSync(root, { recursive: true, force: true });
49
+ }
50
+ };
51
+
36
52
  describe("AkanAppConfig", () => {
37
53
  test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
38
54
  const config = new AkanAppConfig(app, ["shared"], packageJson, {}, baseDevEnv);
@@ -340,8 +356,7 @@ describe("AkanAppConfig", () => {
340
356
  expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
341
357
  `postgres@${runtimeDependencies.postgres}`,
342
358
  ]);
343
- // The workspace-root install covers the toolchain/runtime plus every app Capacitor plugin
344
- // (deduped — "@capacitor/core" appears in both source lists).
359
+ // The workspace-root install covers the toolchain plus every app Capacitor plugin, deduped (`@capacitor/core`).
345
360
  const expectedMobilePackages: string[] = [
346
361
  "@capacitor/cli",
347
362
  "@capacitor/core",
@@ -491,25 +506,13 @@ describe("AkanAppConfig lib externalLibs", () => {
491
506
  });
492
507
 
493
508
  test("reads them off every workspace lib config on load", async () => {
494
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
495
- try {
496
- fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
497
- fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
498
- fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
499
- fs.writeFileSync(
500
- path.join(root, "libs/extlib/akan.config.ts"),
501
- "export default { externalLibs: ['puppeteer'] };\n",
502
- );
503
- fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
504
- fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
505
-
506
- const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
507
- const config = await AppExecutor.from(workspace, "extapp").getConfig();
509
+ const config = await loadExtAppConfig(
510
+ "akan-config-libext-",
511
+ "export default { externalLibs: ['shiki'] };\n",
512
+ "export default { externalLibs: ['puppeteer'] };\n",
513
+ );
508
514
 
509
- expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
510
- } finally {
511
- fs.rmSync(root, { recursive: true, force: true });
512
- }
515
+ expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
513
516
  });
514
517
  });
515
518
 
@@ -553,26 +556,14 @@ describe("AkanAppConfig lib docker runs", () => {
553
556
  });
554
557
 
555
558
  test("reads them off every workspace lib config on load", async () => {
556
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libdocker-"));
557
- try {
558
- fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
559
- fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
560
- fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default {};\n");
561
- fs.writeFileSync(
562
- path.join(root, "libs/extlib/akan.config.ts"),
563
- "export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
564
- );
565
- fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
566
- fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
567
-
568
- const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
569
- const config = await AppExecutor.from(workspace, "extapp").getConfig();
559
+ const config = await loadExtAppConfig(
560
+ "akan-config-libdocker-",
561
+ "export default {};\n",
562
+ "export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
563
+ );
570
564
 
571
- expect(config.dockerfile).toContain("RUN echo from-lib");
572
- expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
573
- } finally {
574
- fs.rmSync(root, { recursive: true, force: true });
575
- }
565
+ expect(config.dockerfile).toContain("RUN echo from-lib");
566
+ expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
576
567
  });
577
568
  });
578
569