@akanjs/devkit 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
package/fileEditor.ts CHANGED
@@ -17,57 +17,29 @@ export class FileEditor {
17
17
  }
18
18
 
19
19
  find(pattern: string | RegExp): number {
20
- const lines = this.#content.split("\n");
21
20
  const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
22
-
23
- for (let i = 0; i < lines.length; i++) {
24
- const line = lines[i];
25
- if (!line) continue;
26
- if (regex.test(line)) return i;
27
- }
28
-
29
- return -1;
21
+ return this.#content.split("\n").findIndex((line) => !!line && regex.test(line));
30
22
  }
31
23
 
32
24
  findAll(pattern: string | RegExp): number[] {
33
- const lines = this.#content.split("\n");
34
25
  const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
35
- const matches: number[] = [];
36
-
37
- for (let i = 0; i < lines.length; i++) {
38
- const line = lines[i];
39
- if (!line) continue;
40
- if (regex.test(line)) matches.push(i);
41
- }
42
-
43
- return matches;
26
+ return this.#content.split("\n").flatMap((line, index) => (line && regex.test(line) ? [index] : []));
44
27
  }
45
28
 
46
29
  insertAfter(pattern: string | RegExp, data: string): this {
47
- const lineIndex = this.find(pattern);
48
-
49
- if (lineIndex === -1) {
50
- throw new Error(`Pattern not found: ${pattern}`);
51
- }
52
-
53
- const lines = this.#content.split("\n");
54
- lines.splice(lineIndex + 1, 0, data);
55
- this.#content = lines.join("\n");
56
-
57
- return this;
30
+ return this.#insertAt(pattern, data, 1);
58
31
  }
59
32
 
60
33
  insertBefore(pattern: string | RegExp, data: string): this {
61
- const lineIndex = this.find(pattern);
62
-
63
- if (lineIndex === -1) {
64
- throw new Error(`Pattern not found: ${pattern}`);
65
- }
34
+ return this.#insertAt(pattern, data, 0);
35
+ }
66
36
 
37
+ #insertAt(pattern: string | RegExp, data: string, offset: number): this {
38
+ const lineIndex = this.find(pattern);
39
+ if (lineIndex === -1) throw new Error(`Pattern not found: ${pattern}`);
67
40
  const lines = this.#content.split("\n");
68
- lines.splice(lineIndex, 0, data);
41
+ lines.splice(lineIndex + offset, 0, data);
69
42
  this.#content = lines.join("\n");
70
-
71
43
  return this;
72
44
  }
73
45
 
@@ -1,12 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import path from "node:path";
3
- import { getDirname } from "./getDirname";
3
+ import { pathToFileURL } from "node:url";
4
+ import { getDirname } from "./fileSys";
4
5
 
5
6
  describe("getDirname", () => {
6
7
  test("converts file URLs to filesystem paths", () => {
7
- const dirname = getDirname("file:///tmp/akan%20workspace/index.ts");
8
+ const filePath = path.resolve("/tmp/akan workspace/index.ts");
9
+ const dirname = getDirname(pathToFileURL(filePath).href);
8
10
 
9
- expect(dirname).toBe(path.join("/tmp", "akan workspace"));
11
+ expect(dirname).toBe(path.dirname(filePath));
10
12
  });
11
13
 
12
14
  test("keeps Windows drive paths valid when running on Windows", () => {
package/fileSys.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { lstat, rename, rm, stat } from "node:fs/promises";
2
+ import nodePath from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { Logger } from "akanjs/common";
3
5
 
6
+ export const getDirname = (url: string) => nodePath.dirname(fileURLToPath(url));
7
+
4
8
  export class FileSys {
5
9
  static logger = new Logger("FileSys");
6
10
  static async fileExists(path: string) {
@@ -39,24 +43,33 @@ export class FileSys {
39
43
  static async writeText(path: string, content: string) {
40
44
  return await Bun.file(path).write(content);
41
45
  }
42
- /**
43
- * Replaces a file in one `rename`, so a concurrent reader sees either the old bytes or the new ones.
44
- * Generated barrels are written by every dev server watching the same `libs/` tree while those same
45
- * servers' watchers are reading them, and a half-written barrel reads back as a user edit.
46
- *
47
- * The temp file is a sibling because `rename` is only atomic within a filesystem, and it lands in a
48
- * watched directory — the `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
49
- */
46
+ // One `rename`, so a watcher never reads a half-written barrel as a user edit. The temp is a sibling (rename is atomic
47
+ // only within a filesystem), and its `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
50
48
  static async writeTextAtomic(filePath: string, content: string) {
51
49
  const temp = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
52
50
  try {
53
51
  await Bun.write(temp, content);
54
- await rename(temp, filePath);
52
+ await FileSys.replace(temp, filePath);
55
53
  } catch (error) {
56
54
  await rm(temp, { force: true }).catch(() => undefined);
57
55
  throw error;
58
56
  }
59
57
  }
58
+ static readonly #replaceAttempts = 100;
59
+ // Windows refuses to rename over a file another process has open (EPERM, or EACCES/EBUSY). A reader holds it for one
60
+ // read, so the rename is retried for a few seconds instead of failing.
61
+ static async replace(temp: string, target: string) {
62
+ for (let attempt = 1; ; attempt++) {
63
+ try {
64
+ return await rename(temp, target);
65
+ } catch (error) {
66
+ const code = (error as NodeJS.ErrnoException).code;
67
+ const busy = process.platform === "win32" && (code === "EPERM" || code === "EACCES" || code === "EBUSY");
68
+ if (!busy || attempt >= FileSys.#replaceAttempts) throw error;
69
+ await Bun.sleep(Math.min(10 * attempt, 50));
70
+ }
71
+ }
72
+ }
60
73
  static async writeJson(path: string, content: object) {
61
74
  return await Bun.file(path).write(`${JSON.stringify(content, null, 2)}\n`);
62
75
  }
@@ -1,22 +1,9 @@
1
1
  import ts from "typescript";
2
2
  import type { QualityWarning, SourceFileInfo } from "./qualityScanner";
3
3
 
4
- /**
5
- * The inventory of model fields a screen writes but does not publish.
6
- *
7
- * A control handed `onChange={st.do.setTitleOnTask}` **by reference** names the field it writes, so it emits
8
- * `data-akan-action` and `useFieldTool` publishes the field to the in-page agent. Any wrapper around that setter is
9
- * an anonymous closure carrying neither, and the field goes quiet — for the agent, for an E2E selector, and for the
10
- * accessibility tree.
11
- *
12
- * `no-unpublished-form-setter.grit` is the per-line enforcement, and it fires only on a pure forwarding wrapper,
13
- * because every other shape has a legitimate reading and a lint error would be wrong. This is the other half: the
14
- * per-file count of fields that ended up unreachable whatever the reason, which is the number worth watching. A
15
- * warning rather than an error for the same reason — the remedy depends on why the wrapper is there.
16
- *
17
- * Only a handler that takes a parameter is counted. A zero-parameter handler (`onClick={() => st.do.setStatusOnUser
18
- * ("active")}`) is a button setting a constant, not a form control, and its remedy is an `st.tool` beside it.
19
- */
4
+ // Only a setter passed by reference publishes its field (`data-akan-action`, agent tool). The grit rule flags only pure
5
+ // forwarding wrappers; this counts every field left unpublished whatever the reason. A zero-parameter handler is a
6
+ // button setting a constant, not a form control, so it is not counted.
20
7
  export class FormSetterScanner {
21
8
  static #fieldSetter = /^set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*$/;
22
9
 
@@ -14,11 +14,6 @@ export interface BuildAllRoutesResult {
14
14
  seedIndex: RouteSeedIndex;
15
15
  }
16
16
 
17
- /**
18
- * Walk every route in `pages` and produce its client bundle up-front.
19
- * Intended to run under `akan build` (production) so the serve path
20
- * never needs to compile.
21
- */
22
17
  export class AllRoutesBuilder {
23
18
  #app: App;
24
19
  #artifact: BaseBuildArtifact;
@@ -47,23 +42,16 @@ export class AllRoutesBuilder {
47
42
  this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
48
43
  this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
49
44
 
50
- // Discovery first, bundling second. Chunk splitting only dedupes within one `Bun.build`, so a
51
- // dependency shared by entries from different routes was emitted once per route that reached it.
52
- // Discovery is cached and does no bundling, so collecting every entry up front costs almost nothing.
53
- const allEntries: string[] = [];
54
- const seen = new Set<string>();
45
+ // Every route's entries go into one Bun.build: chunk splitting only dedupes within a single build.
46
+ const allEntries = new Set<string>();
55
47
  for (const entry of seedIndex.entries) {
56
48
  const seeds = Array.from(new Set([...seedIndex.globalLayoutFiles, ...entry.seeds]));
57
- for (const discovered of await this.#discovery.discover(seeds)) {
58
- if (seen.has(discovered)) continue;
59
- seen.add(discovered);
60
- allEntries.push(discovered);
61
- }
49
+ for (const discovered of await this.#discovery.discover(seeds)) allEntries.add(discovered);
62
50
  this.#routeIds.push(entry.routeId);
63
51
  }
64
- this.#app.verbose(`[build-all] ${allEntries.length} client entries across ${this.#routeIds.length} routes`);
52
+ this.#app.verbose(`[build-all] ${allEntries.size} client entries across ${this.#routeIds.length} routes`);
65
53
 
66
- const delta = await this.#buildEntries(allEntries);
54
+ const delta = await this.#buildEntries([...allEntries]);
67
55
  this.#mergeDelta(delta);
68
56
  this.#merged.knownEntries = Array.from(this.#knownSet);
69
57
 
@@ -113,10 +101,8 @@ export class AllRoutesBuilder {
113
101
  }
114
102
 
115
103
  #mergeDelta(delta: BuildRouteClientResult): void {
116
- for (const [key, row] of Object.entries(delta.manifestDelta)) this.#merged.clientManifest[key] = row;
117
- for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap)) {
118
- this.#merged.ssrManifest.moduleMap[url] = byName;
119
- }
104
+ Object.assign(this.#merged.clientManifest, delta.manifestDelta);
105
+ Object.assign(this.#merged.ssrManifest.moduleMap, delta.ssrManifestDelta.moduleMap);
120
106
  for (const abs of delta.newEntries) this.#knownSet.add(abs);
121
107
  }
122
108
  }
@@ -1,21 +1,18 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { tempDirs } from "../testHelpers";
5
5
  import { AutoImportSync, transformSource } from "./autoImportSync";
6
6
 
7
- const tempRoots: string[] = [];
7
+ const makeTempRoot = tempDirs("akan-auto-import-");
8
8
 
9
- const makeTempRoot = async () => {
10
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-auto-import-"));
11
- tempRoots.push(root);
12
- return root;
9
+ const seed = async (root: string, rel: string, content: string) => {
10
+ const abs = path.join(root, rel);
11
+ await mkdir(path.dirname(abs), { recursive: true });
12
+ await writeFile(abs, content);
13
+ return abs;
13
14
  };
14
15
 
15
- afterEach(async () => {
16
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
17
- });
18
-
19
16
  const constantCtx = { role: "constant", scope: "libs", project: "shared" } as const;
20
17
  const documentCtx = { role: "document", scope: "libs", project: "shared" } as const;
21
18
  const serviceCtx = { role: "service", scope: "libs", project: "shared" } as const;
@@ -161,6 +158,19 @@ describe("transformSource — client role (package client entry)", () => {
161
158
  );
162
159
  });
163
160
 
161
+ test("never claims fetch from a file that calls the platform fetch", () => {
162
+ const globalOnly = `"use client";\n\nexport const toBlob = async (url: string) => await (await fetch(url)).blob();\n`;
163
+ const mixed = `"use client";\n\nexport const C = async (url: string) => (await fetch(url)).ok && fetch.me() && st.foo;\n`;
164
+ const passed = `"use client";\n\nexport const load = (get = fetch) => get("/a");\n`;
165
+
166
+ expect(transformSource(globalOnly, "CropImage.tsx", clientLibCtx)).toBeNull();
167
+ expect(transformSource(mixed, "C.Zone.tsx", clientLibCtx)).toBe(
168
+ `"use client";\nimport { st } from "@libs/shared/client";\n\nexport const C = async (url: string) => (await fetch(url)).ok && fetch.me() && st.foo;\n`,
169
+ );
170
+ expect(transformSource(passed, "load.ts", clientLibCtx)).toBeNull();
171
+ expect(transformSource(globalOnly, "x.store.ts", storeCtx)).toBeNull();
172
+ });
173
+
164
174
  test("handles webkit .ts files (no jsx) the same as tsx", () => {
165
175
  const source = `import { getEnv } from "akanjs/base";\n\nexport const load = () => fetch.me();\n`;
166
176
  const out = transformSource(source, "cookie.ts", clientLibCtx);
@@ -171,13 +181,6 @@ describe("transformSource — client role (package client entry)", () => {
171
181
  });
172
182
 
173
183
  describe("AutoImportSync.syncForBatch", () => {
174
- const seed = async (root: string, rel: string, content: string) => {
175
- const abs = path.join(root, rel);
176
- await mkdir(path.dirname(abs), { recursive: true });
177
- await writeFile(abs, content);
178
- return abs;
179
- };
180
-
181
184
  test("writes the fix once, then is idempotent", async () => {
182
185
  const root = await makeTempRoot();
183
186
  const abs = await seed(
@@ -296,9 +299,7 @@ describe("AutoImportSync.syncForBatch", () => {
296
299
 
297
300
  test("leaves JS globals and ambiguous domain names alone", async () => {
298
301
  const root = await makeTempRoot();
299
- // `Map` is a real model here but must not shadow the JS global.
300
302
  await seed(root, "libs/x/lib/map/map.constant.ts", `export class Map extends via {}\n`);
301
- // `Dup` is exported by two files → ambiguous → skipped.
302
303
  await seed(root, "libs/x/lib/a/a.constant.ts", `export class Dup {}\n`);
303
304
  await seed(root, "libs/x/lib/b/b.constant.ts", `export class Dup {}\n`);
304
305
  const consumer = await seed(
@@ -335,7 +336,7 @@ describe("AutoImportSync.syncForBatch", () => {
335
336
 
336
337
  test("srvkit: does not import a barrel whose file is absent", async () => {
337
338
  const root = await makeTempRoot();
338
- await seed(root, "libs/x/lib/dict.ts", `export const x = 1;\n`); // no lib/srv.ts
339
+ await seed(root, "libs/x/lib/dict.ts", `export const x = 1;\n`);
339
340
  const file = await seed(root, "libs/x/srvkit/api.ts", `export const f = () => Err.of(srv.user);\n`);
340
341
  const sync = new AutoImportSync({ workspaceRoot: root });
341
342
 
@@ -357,20 +358,11 @@ describe("AutoImportSync.syncForBatch", () => {
357
358
  });
358
359
 
359
360
  describe("AutoImportSync — scope", () => {
360
- const writeAt = async (root: string, rel: string, source: string) => {
361
- const abs = path.join(root, rel);
362
- await mkdir(path.dirname(abs), { recursive: true });
363
- await writeFile(abs, source);
364
- return abs;
365
- };
366
-
367
- // `lib/__lib/` is generated and gitignored: `akan sync` rewrites it, which is also what makes the
368
- // watcher hand it to the syncer, so an edit there is churn that the next sync discards.
369
361
  test("leaves the generated lib/__lib stubs alone and still syncs lib/__scalar", async () => {
370
362
  const root = await makeTempRoot();
371
363
  const needsImport = "export class A {\n count = field(Int);\n}\n";
372
- const generated = await writeAt(root, "libs/shared/lib/__lib/lib.constant.ts", needsImport);
373
- const scalar = await writeAt(root, "libs/shared/lib/__scalar/money/money.constant.ts", needsImport);
364
+ const generated = await seed(root, "libs/shared/lib/__lib/lib.constant.ts", needsImport);
365
+ const scalar = await seed(root, "libs/shared/lib/__scalar/money/money.constant.ts", needsImport);
374
366
 
375
367
  const result = await new AutoImportSync({ workspaceRoot: root }).syncForBatch([generated, scalar]);
376
368
 
@@ -380,11 +372,9 @@ describe("AutoImportSync — scope", () => {
380
372
  expect(await readFile(scalar, "utf8")).toContain('import { Int } from "akanjs/base";');
381
373
  });
382
374
 
383
- // The transform is pure and the write is conditional on a difference, so a second pass over its own
384
- // output writes nothing — without that the watcher would re-fire on the syncer's own edit.
385
375
  test("a second pass over its own output changes nothing", async () => {
386
376
  const root = await makeTempRoot();
387
- const file = await writeAt(
377
+ const file = await seed(
388
378
  root,
389
379
  "libs/shared/lib/task/task.constant.ts",
390
380
  "export class A {\n count = field(Int);\n}\n",
@@ -397,11 +387,10 @@ describe("AutoImportSync — scope", () => {
397
387
  expect(await readFile(file, "utf8")).toBe(afterFirst);
398
388
  });
399
389
 
400
- // One try/catch per file: a file that cannot be read is reported and the rest of the batch still runs.
401
390
  test("a failing file is reported without abandoning the batch", async () => {
402
391
  const root = await makeTempRoot();
403
392
  const missing = path.join(root, "libs/shared/lib/task/gone.constant.ts");
404
- const good = await writeAt(
393
+ const good = await seed(
405
394
  root,
406
395
  "libs/shared/lib/task/task.constant.ts",
407
396
  "export class A {\n count = field(Int);\n}\n",
@@ -3,11 +3,7 @@ import path from "node:path";
3
3
  import ts from "typescript";
4
4
  import { FileSys } from "../fileSys";
5
5
 
6
- //* Auto-import daemon: a source-editing sibling of DevGeneratedIndexSync. When a domain file changes,
7
- //* framework symbols that are used but not imported (e.g. `Int` in a *.constant.ts, `fetch` in a *.store.ts)
8
- //* are inserted automatically. Detection is a closed-registry scan: we only look for a fixed, framework-owned
9
- //* set of identifiers per file role, so no type information is required. Writes only happen on a real diff,
10
- //* which keeps the watcher from looping on its own edits.
6
+ //* Closed-registry scan (no type information); writes only on a real diff, or the watcher loops on its own edits.
11
7
 
12
8
  export interface AutoImportSyncResult {
13
9
  changedFiles: string[];
@@ -39,13 +35,9 @@ interface FileContext {
39
35
  interface ImportTarget {
40
36
  specifier: string;
41
37
  kind: ImportKind;
42
- //* When true, emitted as a type-only import (`import type { X }`, or inline `type X` when mixed with
43
- //* value names from the same specifier). Only meaningful for `kind: "named"`.
44
38
  typeOnly?: boolean;
45
39
  }
46
40
 
47
- //* A registry rule: the identifiers `names` should be imported from `specifier` with the given `kind`.
48
- //* `specifier` may depend on the file's package (e.g. the client entrypoint).
49
41
  interface ImportRule {
50
42
  names: string[];
51
43
  specifier: string | ((ctx: FileContext) => string);
@@ -55,23 +47,19 @@ interface ImportRule {
55
47
 
56
48
  const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx)$/;
57
49
 
58
- //* Shared rules reused across roles. Domain (`lib/<model>/`) files sit at a fixed depth, so the
59
- //* relative barrels `../cnst`, `../db`, `../srv`, `../dict` are always correct for them.
50
+ //* Domain files sit at a fixed depth (`lib/<model>/`), so the relative barrels below are always correct for them.
60
51
  const AKAN_BASE: ImportRule = {
61
52
  names: ["Int", "Float", "ID", "Any", "Upload", "enumOf", "dayjs"],
62
53
  specifier: "akanjs/base",
63
54
  kind: "named",
64
55
  };
65
- //* Type-only exports of akanjs/base — emitted as `type` imports (e.g. `import { type Dayjs, dayjs }`).
66
56
  const AKAN_BASE_TYPES: ImportRule = { names: ["Dayjs"], specifier: "akanjs/base", kind: "named", typeOnly: true };
67
57
  const CNST_NS: ImportRule = { names: ["cnst"], specifier: "../cnst", kind: "namespace" };
68
58
  const DB_NS: ImportRule = { names: ["db"], specifier: "../db", kind: "namespace" };
69
59
  const SRV_NS: ImportRule = { names: ["srv"], specifier: "../srv", kind: "namespace" };
70
60
  const ERR_DICT: ImportRule = { names: ["Err"], specifier: "../dict", kind: "named" };
71
61
 
72
- //* The closed registry, keyed by file role. Derived from an import-frequency scan across apps/libs;
73
- //* only high-confidence, framework-owned identifiers are listed (domain model/scalar refs are resolved
74
- //* separately). Order matters only when the same name could appear twice — keep names unique per role.
62
+ //* Keep names unique per role: targetFor takes the first rule that lists a name.
75
63
  const RULES: Record<FileRole, ImportRule[]> = {
76
64
  constant: [AKAN_BASE, AKAN_BASE_TYPES, { names: ["via"], specifier: "akanjs/constant", kind: "named" }],
77
65
  document: [
@@ -143,8 +131,6 @@ const RULES: Record<FileRole, ImportRule[]> = {
143
131
  ],
144
132
  };
145
133
 
146
- //* Domain imports resolve open-ended model/scalar class refs against a per-package index, rather than
147
- //* a fixed registry. Only these roles opt in, and each may pull from the listed sibling file kinds.
148
134
  type DomainKind = "constant" | "document" | "signal";
149
135
  type DomainIndex = Map<string, { file: string; kind: DomainKind }[]>;
150
136
  const PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
@@ -153,17 +139,17 @@ const DOMAIN_ROLE_KINDS: Partial<Record<FileRole, DomainKind[]>> = {
153
139
  dictionary: ["constant", "document", "signal"],
154
140
  common: ["constant"],
155
141
  };
156
- //* lib barrel imports (srvkit/common): identifier -> generated `lib/<barrel>.ts` file + import shape.
157
- //* Their specifier depth is per-file (see `#libBarrelResolver`), so they can't live in the RULES table.
142
+ //* srvkit/common import the lib barrels from a per-file depth, so they cannot live in the RULES table.
158
143
  const LIB_BARRELS: Record<string, { barrel: string; kind: ImportKind }> = {
159
144
  Err: { barrel: "dict", kind: "named" },
160
145
  db: { barrel: "db", kind: "namespace" },
161
146
  cnst: { barrel: "cnst", kind: "namespace" },
162
147
  srv: { barrel: "srv", kind: "namespace" },
163
148
  };
164
- //* ECMAScript/TS structural globals never resolved as domain symbols, so that a package model named
165
- //* e.g. `Map` cannot shadow the JS `Map` used in `new Map()`. Domain-y globals like `File` are allowed
166
- //* on purpose (they are real models here).
149
+ //* Importing Akan's `fetch` over the platform global silently rebinds `fetch(url)`, so such a name is claimed
150
+ //* only when every reference is member-shaped (`fetch.viewX()`).
151
+ const GLOBAL_NAMES = new Set(["fetch"]);
152
+ //* Structural globals a package model must not shadow (`new Map()`); `File` is left out on purpose — it is a real model.
167
153
  const DOMAIN_DENYLIST = new Set([
168
154
  "Map",
169
155
  "Set",
@@ -204,7 +190,6 @@ const DOMAIN_DENYLIST = new Set([
204
190
 
205
191
  export class AutoImportSync {
206
192
  readonly #workspaceRoot: string;
207
- //* Per-package domain index (symbol -> source file), invalidated when a package's model file changes.
208
193
  readonly #domainCache = new Map<string, DomainIndex>();
209
194
 
210
195
  constructor({ workspaceRoot }: AutoImportSyncOptions) {
@@ -216,8 +201,6 @@ export class AutoImportSync {
216
201
  const errors: string[] = [];
217
202
  const seen = new Set<string>();
218
203
 
219
- // Drop cached domain indexes for any package whose model files changed in this batch, so a newly
220
- // added/renamed model is visible to references resolved later in the same batch.
221
204
  for (const file of files) {
222
205
  const pkgRoot = this.#domainPkgRootOf(file);
223
206
  if (pkgRoot) this.#domainCache.delete(pkgRoot);
@@ -240,8 +223,6 @@ export class AutoImportSync {
240
223
  return { changedFiles, errors };
241
224
  }
242
225
 
243
- //* Resolve the role + package location for a file, or null when the file is out of scope
244
- //* (outside apps/libs, a generated/declaration/test file, or a facet+extension we do not handle).
245
226
  #contextFor(abs: string): FileContext | null {
246
227
  const rel = path.relative(this.#workspaceRoot, abs);
247
228
  if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
@@ -251,9 +232,7 @@ export class AutoImportSync {
251
232
  const parts = rel.split(path.sep).filter(Boolean);
252
233
  const [scope, project, facet] = parts;
253
234
  if ((scope !== "apps" && scope !== "libs") || !project || !facet) return null;
254
- //* `lib/__lib/` holds the generated per-lib re-export stubs — gitignored, and rewritten by every
255
- //* `akan sync`, which is also what makes the watcher report them. An edit there is churn at best and
256
- //* silently discarded at worst. `lib/__scalar/` is real source and stays in scope.
235
+ //* `lib/__lib/` is rewritten by every `akan sync`, so an edit there is discarded; `lib/__scalar/` is real source.
257
236
  if (facet === "lib" && parts[3] === "__lib") return null;
258
237
  const role = roleFor(facet, base);
259
238
  if (!role) return null;
@@ -272,10 +251,6 @@ export class AutoImportSync {
272
251
  return true;
273
252
  }
274
253
 
275
- //* Build the dynamic resolver for identifiers the static registry does not cover, or undefined when
276
- //* the role has none. `srvkit`/`common` resolve the package's generated lib barrels (variable depth);
277
- //* the domain roles resolve open-ended model/scalar refs against the per-package index. `common`
278
- //* uses both — barrels first, then domain (the two symbol sets are disjoint).
279
254
  async #extraResolverFor(abs: string, ctx: FileContext) {
280
255
  const resolvers: ((symbol: string) => ImportTarget | null)[] = [];
281
256
  if (ctx.role === "srvkit" || ctx.role === "common") resolvers.push(await this.#libBarrelResolver(abs, ctx));
@@ -291,22 +266,17 @@ export class AutoImportSync {
291
266
  };
292
267
  }
293
268
 
294
- //* Resolve an unbound PascalCase identifier to a relative import of the package model/scalar that
295
- //* exports it (only when exactly one package file of an allowed kind exports that name).
296
269
  async #domainResolver(abs: string, ctx: FileContext, kinds: DomainKind[]) {
297
270
  const index = await this.#domainIndex(path.join(this.#workspaceRoot, ctx.scope, ctx.project));
298
271
  const fileDir = path.dirname(abs);
299
272
  return (symbol: string): ImportTarget | null => {
300
273
  if (!PASCAL_CASE_RE.test(symbol) || DOMAIN_DENYLIST.has(symbol)) return null;
301
274
  const entries = (index.get(symbol) ?? []).filter((entry) => kinds.includes(entry.kind));
302
- if (entries.length !== 1) return null; // unknown or ambiguous → leave it alone
275
+ if (entries.length !== 1) return null;
303
276
  return { specifier: relativeSpecifier(fileDir, entries[0].file), kind: "named" };
304
277
  };
305
278
  }
306
279
 
307
- //* srvkit/common files import the package's generated lib barrels from a variable depth
308
- //* (`../lib/dict`, `../../lib/dict`, …). Resolve each barrel name to a path relative to this file,
309
- //* but only for barrels that actually exist (so an absent `lib/srv.ts` never yields a broken import).
310
280
  async #libBarrelResolver(abs: string, ctx: FileContext) {
311
281
  const fileDir = path.dirname(abs);
312
282
  const libDir = path.join(this.#workspaceRoot, ctx.scope, ctx.project, "lib");
@@ -326,7 +296,6 @@ export class AutoImportSync {
326
296
  return index;
327
297
  }
328
298
 
329
- //* The apps/libs package root for a model file (`*.constant/document/signal.ts` under `lib/`), else null.
330
299
  #domainPkgRootOf(file: string): string | null {
331
300
  const rel = path.relative(this.#workspaceRoot, path.resolve(file));
332
301
  if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
@@ -337,9 +306,6 @@ export class AutoImportSync {
337
306
  }
338
307
  }
339
308
 
340
- //* Which facet+extension combinations get auto-imports, and the role that drives their registry.
341
- //* `client` covers every file that pulls domain symbols from the package client entrypoint:
342
- //* lib UI (.tsx), the `ui`/`page` facets (.tsx), and the `webkit` facet (.ts/.tsx).
343
309
  const roleFor = (facet: string, base: string): FileRole | null => {
344
310
  if (facet === "lib") {
345
311
  if (base.endsWith(".constant.ts")) return "constant";
@@ -367,8 +333,7 @@ const targetFor = (symbol: string, ctx: FileContext): ImportTarget | null => {
367
333
  return null;
368
334
  };
369
335
 
370
- //* Returns the rewritten source, or null when nothing needs to change. `resolveExtra` (when supplied)
371
- //* handles identifiers not matched by the framework registry — domain model refs and srvkit barrels.
336
+ /** The rewritten source, or null when nothing changes. */
372
337
  export const transformSource = (
373
338
  source: string,
374
339
  fileName: string,
@@ -378,13 +343,12 @@ export const transformSource = (
378
343
  const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindFor(fileName));
379
344
  const bound = collectBoundNames(sf);
380
345
  const used = collectUsedReferences(sf);
346
+ const bareGlobals = collectBareGlobalReferences(sf);
381
347
 
382
- // Group the needed symbols by their import target: framework registry first, then dynamic resolver.
383
- // Named groups map each name to whether it is type-only, so we can emit `import type`/inline `type`.
384
348
  const namedBySpecifier = new Map<string, Map<string, boolean>>();
385
349
  const namespaceImports: { name: string; specifier: string }[] = [];
386
350
  for (const name of used) {
387
- if (bound.has(name)) continue;
351
+ if (bound.has(name) || bareGlobals.has(name)) continue;
388
352
  const target = targetFor(name, ctx) ?? resolveExtra?.(name) ?? null;
389
353
  if (!target) continue;
390
354
  if (target.kind === "namespace") namespaceImports.push({ name, specifier: target.specifier });
@@ -418,8 +382,7 @@ export const transformSource = (
418
382
  for (const ns of namespaceImports) newStatements.push(`import * as ${ns.name} from "${ns.specifier}";`);
419
383
 
420
384
  if (newStatements.length > 0) {
421
- // If the anchor import is itself being merged, fold the new lines into that edit to avoid a
422
- // zero-width insertion sharing a boundary with the merge replacement.
385
+ // A zero-width insertion at the anchor's boundary would collide with the anchor's own merge edit.
423
386
  const anchorEdit = anchor ? edits.find((edit) => edit.start === anchor.getStart(sf)) : undefined;
424
387
  if (anchorEdit) anchorEdit.text = `${anchorEdit.text}\n${newStatements.join("\n")}`;
425
388
  else if (anchor)
@@ -438,8 +401,7 @@ export const transformSource = (
438
401
  return out === source ? null : out;
439
402
  };
440
403
 
441
- //* Every name introduced into scope: import bindings plus local declarations. Over-collecting only
442
- //* suppresses an insertion (safe); under-collecting would risk a duplicate import (unsafe).
404
+ //* Over-collecting only suppresses an insertion; under-collecting would add a duplicate import.
443
405
  const collectBoundNames = (sf: ts.SourceFile): Set<string> => {
444
406
  const names = new Set<string>();
445
407
  const visit = (node: ts.Node) => {
@@ -478,8 +440,19 @@ const collectUsedReferences = (sf: ts.SourceFile): Set<string> => {
478
440
  return used;
479
441
  };
480
442
 
481
- //* True when the identifier reads a binding, as opposed to being a member name, property key, or
482
- //* declaration name. e.g. in `cnst.Coordinate`, `cnst` is a reference but `Coordinate` is not.
443
+ const collectBareGlobalReferences = (sf: ts.SourceFile): Set<string> => {
444
+ const bare = new Set<string>();
445
+ const visit = (node: ts.Node) => {
446
+ if (ts.isIdentifier(node) && GLOBAL_NAMES.has(node.text) && isReferencePosition(node)) {
447
+ const parent = node.parent;
448
+ if (!(parent && ts.isPropertyAccessExpression(parent) && parent.expression === node)) bare.add(node.text);
449
+ }
450
+ ts.forEachChild(node, visit);
451
+ };
452
+ ts.forEachChild(sf, visit);
453
+ return bare;
454
+ };
455
+
483
456
  const isReferencePosition = (node: ts.Identifier): boolean => {
484
457
  const parent = node.parent;
485
458
  if (!parent) return true;
@@ -494,7 +467,7 @@ const isReferencePosition = (node: ts.Identifier): boolean => {
494
467
 
495
468
  interface ExistingNamedImport {
496
469
  decl: ts.ImportDeclaration;
497
- names: Map<string, boolean>; // name -> type-only (whole-clause `import type` or inline `type`)
470
+ names: Map<string, boolean>;
498
471
  }
499
472
 
500
473
  const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<string, ExistingNamedImport> => {
@@ -505,7 +478,7 @@ const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<s
505
478
  if (!clause || !bindings || !ts.isNamedImports(bindings)) continue;
506
479
  if (!ts.isStringLiteral(decl.moduleSpecifier)) continue;
507
480
  const specifier = decl.moduleSpecifier.text;
508
- if (map.has(specifier)) continue; // first same-specifier named import wins as the merge site
481
+ if (map.has(specifier)) continue;
509
482
  const names = new Map<string, boolean>();
510
483
  for (const el of bindings.elements) names.set(el.name.text, clause.isTypeOnly || el.isTypeOnly);
511
484
  map.set(specifier, { decl, names });
@@ -513,8 +486,6 @@ const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<s
513
486
  return map;
514
487
  };
515
488
 
516
- //* Render a named import, hoisting to `import type { … }` when every name is type-only and otherwise
517
- //* prefixing each type-only name with inline `type`. Names sorted case-insensitively (Biome order).
518
489
  const formatNamedImport = (specifier: string, names: Map<string, boolean>): string => {
519
490
  const entries = [...names.entries()].sort((a, b) => compareNames(a[0], b[0]));
520
491
  if (entries.every(([, isType]) => isType))
@@ -534,8 +505,7 @@ const directivePrologueEnd = (sf: ts.SourceFile): number => {
534
505
 
535
506
  const scriptKindFor = (fileName: string) => (fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
536
507
 
537
- //* Case-insensitive primary order with a case-sensitive tie-break (uppercase first), matching Biome's
538
- //* import specifier sort — so e.g. `Dayjs` precedes `dayjs`.
508
+ // Matches Biome's import-specifier sort: case-insensitive, uppercase first on a tie (`Dayjs` before `dayjs`).
539
509
  const compareNames = (a: string, b: string): number => {
540
510
  const [la, lb] = [a.toLowerCase(), b.toLowerCase()];
541
511
  if (la !== lb) return la < lb ? -1 : 1;
@@ -549,8 +519,6 @@ const fileExists = async (file: string) =>
549
519
  .then((s) => s.isFile())
550
520
  .catch(() => false);
551
521
 
552
- // ---- Domain index ---------------------------------------------------------------------------------
553
-
554
522
  const domainKindOf = (base: string): DomainKind | null => {
555
523
  if (base.endsWith(".constant.ts")) return "constant";
556
524
  if (base.endsWith(".document.ts")) return "document";
@@ -558,7 +526,6 @@ const domainKindOf = (base: string): DomainKind | null => {
558
526
  return null;
559
527
  };
560
528
 
561
- //* Scan a package `lib/` for model files and index their exported PascalCase declarations.
562
529
  const buildDomainIndex = async (libDir: string): Promise<DomainIndex> => {
563
530
  const index: DomainIndex = new Map();
564
531
  for (const file of await collectDomainFiles(libDir)) {
@@ -604,7 +571,6 @@ const exportedPascalNames = (source: string, fileName: string): string[] => {
604
571
  return names.filter((name) => PASCAL_CASE_RE.test(name));
605
572
  };
606
573
 
607
- //* Relative module specifier from a file's directory to a target file, extension stripped, `./`-anchored.
608
574
  const relativeSpecifier = (fromDir: string, toFile: string): string => {
609
575
  const rel = path
610
576
  .relative(fromDir, toFile)