@akanjs/devkit 3.0.0-beta.9 → 3.0.1

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 (346) hide show
  1. package/CHANGELOG.md +1819 -0
  2. package/DEV_RUNTIME_KNOBS.md +44 -26
  3. package/README.ko.md +21 -7
  4. package/README.md +17 -8
  5. package/abstractDoc.test.ts +6 -15
  6. package/abstractDoc.ts +6 -8
  7. package/agentsIndex.test.ts +4 -7
  8. package/agentsIndex.ts +6 -22
  9. package/akanApp/BackendImportGraph.test.ts +8 -22
  10. package/akanApp/BackendImportGraph.ts +1 -20
  11. package/akanApp/akanApp.host.test.ts +56 -3
  12. package/akanApp/akanApp.host.ts +218 -278
  13. package/akanApp/devBootLatch.test.ts +39 -0
  14. package/akanApp/devBootLatch.ts +39 -0
  15. package/akanApp/devHostPolicy.test.ts +81 -35
  16. package/akanApp/devHostPolicy.ts +41 -102
  17. package/akanConfig/akanBin.test.ts +62 -0
  18. package/akanConfig/akanBin.ts +78 -0
  19. package/akanConfig/akanConfig.test.ts +439 -141
  20. package/akanConfig/akanConfig.ts +359 -275
  21. package/akanConfig/index.ts +1 -0
  22. package/akanConfig/types.ts +22 -8
  23. package/akanContext.ts +110 -160
  24. package/akanMcpContract.ts +28 -73
  25. package/appSelectionMemory.ts +1 -4
  26. package/applicationBuildReporter.ts +37 -5
  27. package/applicationBuildRunner.test.ts +41 -5
  28. package/applicationBuildRunner.ts +71 -63
  29. package/applicationTestPreload.ts +3 -12
  30. package/artifact/implicitRootLayout.test.ts +34 -31
  31. package/artifact/implicitRootLayout.ts +60 -107
  32. package/artifact/routeSeedIndex.test.ts +5 -18
  33. package/artifact/routeSeedIndex.ts +11 -44
  34. package/biome.base.json +39 -2
  35. package/biomeBase.ts +2 -5
  36. package/cloud/cloudApi.test.ts +202 -0
  37. package/cloud/cloudApi.ts +77 -39
  38. package/cloud/configLock.test.ts +64 -0
  39. package/cloud/configLock.ts +96 -0
  40. package/cloud/constants.ts +26 -5
  41. package/cloud/globalConfig.ts +71 -32
  42. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  43. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  44. package/codeAgent/agent/CodeAgent.ts +551 -0
  45. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  46. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  47. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  48. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  49. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  50. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  51. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  52. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  53. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  54. package/codeAgent/agent/CodeMailbox.ts +148 -0
  55. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  56. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  57. package/codeAgent/agent/SubagentPool.ts +214 -0
  58. package/codeAgent/agent/akanCodeModel.ts +47 -0
  59. package/codeAgent/agent/akanCodePaths.ts +31 -0
  60. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  61. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  62. package/codeAgent/agent/codeAgent.test.ts +178 -0
  63. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  64. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  65. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  66. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  67. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  68. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  69. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  70. package/codeAgent/agent/consoleToStderr.ts +8 -0
  71. package/codeAgent/agent/jsonLines.ts +29 -0
  72. package/codeAgent/agent/stringArg.ts +5 -0
  73. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  74. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  75. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  76. package/codeAgent/feedback/PreviewView.ts +95 -0
  77. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  78. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  79. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  80. package/codeAgent/index.ts +34 -0
  81. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  82. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  83. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  84. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  85. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  86. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  87. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  88. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  89. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  90. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  91. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  92. package/codeAgent/tools/AkanCli.ts +50 -0
  93. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  94. package/codeAgent/tools/AkanEditScope.ts +48 -0
  95. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  96. package/codeAgent/tools/AkanToolPack.ts +92 -0
  97. package/codeAgent/tools/AkanVerifier.ts +48 -0
  98. package/codeAgent/tools/AskToolPack.ts +99 -0
  99. package/codeAgent/tools/MailToolPack.ts +76 -0
  100. package/codeAgent/tools/McpClient.ts +187 -0
  101. package/codeAgent/tools/McpOAuth.ts +233 -0
  102. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  103. package/codeAgent/tools/McpServerConfig.ts +129 -0
  104. package/codeAgent/tools/McpSignIn.ts +158 -0
  105. package/codeAgent/tools/McpTokenStore.ts +81 -0
  106. package/codeAgent/tools/McpToolPack.ts +199 -0
  107. package/codeAgent/tools/SessionToolPack.ts +91 -0
  108. package/codeAgent/tools/WebToolPack.ts +147 -0
  109. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  110. package/codeAgent/tools/askToolPack.test.ts +116 -0
  111. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  112. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  113. package/codegenLock.test.ts +4 -13
  114. package/codegenLock.ts +3 -17
  115. package/commandDecorators/argMeta.ts +1 -5
  116. package/commandDecorators/camelToKebabCase.ts +1 -0
  117. package/commandDecorators/command.ts +46 -102
  118. package/commandDecorators/commandBuilder.ts +30 -61
  119. package/commandDecorators/commandDecorators.test.ts +2 -10
  120. package/commandDecorators/dependencyBuilder.ts +10 -1
  121. package/commandDecorators/helpFormatter.ts +68 -145
  122. package/commandDecorators/index.ts +0 -2
  123. package/commandDecorators/targetMeta.ts +9 -7
  124. package/createTunnel.ts +7 -14
  125. package/csrE2e/csrE2eHarness.fixture.ts +506 -0
  126. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  127. package/csrE2e/csrPageStack.e2e.test.ts +265 -0
  128. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  129. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  130. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  131. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  132. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  133. package/dependencyScanner.test.ts +4 -16
  134. package/dependencyScanner.ts +31 -80
  135. package/devkitUtils.test.ts +48 -20
  136. package/executors.linkFallback.test.ts +103 -0
  137. package/executors.test.ts +176 -106
  138. package/executors.ts +212 -498
  139. package/fileEditor.ts +9 -37
  140. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  141. package/fileSys.ts +22 -9
  142. package/formSetterScanner.ts +3 -16
  143. package/frontendBuild/allRoutesBuilder.ts +7 -21
  144. package/frontendBuild/autoImportSync.test.ts +27 -38
  145. package/frontendBuild/autoImportSync.ts +31 -65
  146. package/frontendBuild/buildRouteClient.test.ts +123 -37
  147. package/frontendBuild/bundleDefine.test.ts +12 -0
  148. package/frontendBuild/bundleDefine.ts +16 -0
  149. package/frontendBuild/clientBuildTypes.ts +5 -36
  150. package/frontendBuild/clientEntriesBundler.ts +9 -40
  151. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  152. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  153. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  154. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  155. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  156. package/frontendBuild/csrDevBundler.test.ts +66 -0
  157. package/frontendBuild/csrDevBundler.ts +95 -0
  158. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  159. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  160. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  161. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  162. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  163. package/frontendBuild/csrDevPatcher.ts +548 -0
  164. package/frontendBuild/csrDevPaths.ts +145 -0
  165. package/frontendBuild/csrDevResolver.test.ts +239 -0
  166. package/frontendBuild/csrDevResolver.ts +363 -0
  167. package/frontendBuild/csrDevTypes.ts +61 -0
  168. package/frontendBuild/csrEntryFiles.ts +24 -0
  169. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  170. package/frontendBuild/cssCandidateCache.ts +6 -41
  171. package/frontendBuild/cssCompiler.ts +31 -82
  172. package/frontendBuild/cssImportResolver.ts +6 -19
  173. package/frontendBuild/devChangePlanner.ts +7 -10
  174. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  175. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  176. package/frontendBuild/devRegistryBundler.ts +192 -0
  177. package/frontendBuild/fontOptimizer.test.ts +6 -14
  178. package/frontendBuild/fontOptimizer.ts +15 -72
  179. package/frontendBuild/fontPruner.test.ts +5 -10
  180. package/frontendBuild/fontPruner.ts +5 -20
  181. package/frontendBuild/frontendBuild.test.ts +57 -70
  182. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  183. package/frontendBuild/hmrWatcher.test.ts +59 -32
  184. package/frontendBuild/hmrWatcher.ts +37 -74
  185. package/frontendBuild/index.ts +4 -0
  186. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  187. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  188. package/frontendBuild/precompressArtifacts.ts +5 -18
  189. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  190. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  191. package/frontendBuild/routeClientBuilder.ts +100 -62
  192. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  193. package/frontendBuild/serverGraphFile.test.ts +77 -0
  194. package/frontendBuild/serverGraphFile.ts +93 -0
  195. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  196. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  197. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  198. package/frontendBuild/ssrDevBundler.ts +85 -0
  199. package/frontendBuild/styleContract.ts +1 -8
  200. package/frontendBuild/themeValidator.ts +4 -26
  201. package/frontendBuild/watchRootResolver.test.ts +23 -42
  202. package/frontendBuild/watchRootResolver.ts +5 -27
  203. package/getRelatedCnsts.ts +12 -92
  204. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  205. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  206. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  207. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  208. package/incrementalBuilder/builderChannel.test.ts +24 -49
  209. package/incrementalBuilder/builderChannel.ts +6 -37
  210. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  211. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  212. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  213. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  214. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  215. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  216. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  217. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  218. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  219. package/index.ts +5 -19
  220. package/integration/devResourceProbe.ts +50 -56
  221. package/integration/devStability.integration.test.ts +111 -202
  222. package/integration/devStabilityHarness.test.ts +13 -28
  223. package/integration/devStabilityHarness.ts +67 -330
  224. package/integration/ssrMemoryProbe.ts +41 -167
  225. package/libSource.test.ts +4 -15
  226. package/libSource.ts +19 -31
  227. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  228. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  229. package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +43 -0
  230. package/lint/__fixtures__/no-static-in-object-light-model/fixture.json +1 -0
  231. package/lint/__fixtures__/no-static-in-object-light-model/good.ts +42 -0
  232. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  233. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  234. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  235. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  236. package/lint/gritRules.test.ts +8 -24
  237. package/lint/no-deep-internal-import.grit +2 -2
  238. package/lint/no-document-cookie.grit +29 -0
  239. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  240. package/lint/no-static-in-object-light-model.grit +27 -0
  241. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  242. package/lint/no-web-storage.grit +30 -0
  243. package/linter.test.ts +1 -2
  244. package/linter.ts +26 -116
  245. package/mobile/desktopBin.test.ts +182 -0
  246. package/mobile/desktopBin.ts +144 -0
  247. package/mobile/desktopServerStage.test.ts +252 -0
  248. package/mobile/desktopServerStage.ts +178 -0
  249. package/mobile/index.ts +9 -0
  250. package/mobile/mobileTarget.test.ts +57 -8
  251. package/mobile/mobileTarget.ts +45 -26
  252. package/mobile/nativeApi.test.ts +30 -0
  253. package/mobile/nativeApi.ts +40 -0
  254. package/mobile/nativeApp.test.ts +184 -0
  255. package/mobile/nativeApp.ts +454 -0
  256. package/mobile/nativeAppLine.test.ts +69 -0
  257. package/mobile/nativeAppLine.ts +71 -0
  258. package/mobile/nativeConfig.test.ts +438 -0
  259. package/mobile/nativeConfig.ts +294 -0
  260. package/mobile/nativePluginFolders.test.ts +81 -0
  261. package/mobile/nativePluginFolders.ts +60 -0
  262. package/mobile/nativeWebDir.test.ts +80 -0
  263. package/mobile/nativeWebDir.ts +66 -0
  264. package/mobile/placeholderAppId.test.ts +13 -0
  265. package/mobile/placeholderAppId.ts +16 -0
  266. package/mobile/usageDescriptions.test.ts +27 -0
  267. package/mobile/usageDescriptions.ts +24 -0
  268. package/package.json +11 -11
  269. package/packageExports.test.ts +3 -18
  270. package/packageExportsMap.ts +3 -19
  271. package/piDependencyPins.test.ts +46 -0
  272. package/platformTest/BunTestReport.test.ts +60 -0
  273. package/platformTest/BunTestReport.ts +65 -0
  274. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  275. package/platformTest/IdleWatchedProcess.ts +73 -0
  276. package/platformTest/LinuxDockerTarget.ts +105 -0
  277. package/platformTest/PlatformTestRun.test.ts +64 -0
  278. package/platformTest/PlatformTestRun.ts +141 -0
  279. package/platformTest/PlatformTestTarget.ts +142 -0
  280. package/platformTest/TestSnapshot.test.ts +72 -0
  281. package/platformTest/TestSnapshot.ts +103 -0
  282. package/platformTest/WindowsSshTarget.ts +151 -0
  283. package/prompter.ts +3 -13
  284. package/qualityScanner.test.ts +9 -26
  285. package/qualityScanner.ts +200 -286
  286. package/recipeScanner.test.ts +7 -12
  287. package/recipeScanner.ts +9 -38
  288. package/repoIdentity.ts +2 -9
  289. package/routeSourceValidator.test.ts +0 -1
  290. package/routeSourceValidator.ts +7 -42
  291. package/scanInfo.ts +91 -225
  292. package/selectModel.ts +1 -2
  293. package/semver.ts +3 -5
  294. package/slicePlanner.test.ts +6 -24
  295. package/slicePlanner.ts +11 -32
  296. package/spinner.test.ts +3 -8
  297. package/spinner.ts +4 -16
  298. package/sshTunnel.ts +1 -6
  299. package/ssrScanner.test.ts +1 -6
  300. package/ssrScanner.ts +103 -113
  301. package/stripAnsi.ts +3 -0
  302. package/subspace.test.ts +19 -26
  303. package/subspace.ts +48 -115
  304. package/subspaceConfig.ts +3 -11
  305. package/testHelpers.ts +194 -0
  306. package/transforms/asyncDefaultExportDetector.ts +3 -11
  307. package/transforms/barrelAnalyzer.ts +43 -135
  308. package/transforms/barrelImportsPlugin.ts +38 -175
  309. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  310. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  311. package/transforms/moduleSyntax.ts +32 -0
  312. package/transforms/rscUseClientTransform.ts +42 -20
  313. package/transforms/serverEnvPlugin.test.ts +103 -0
  314. package/transforms/serverEnvPlugin.ts +41 -0
  315. package/transforms/transforms.test.ts +166 -44
  316. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  317. package/transforms/useClientBundlePlugin.ts +6 -23
  318. package/typeChecker.ts +7 -63
  319. package/types.ts +2 -0
  320. package/uploadRelease.ts +10 -25
  321. package/workflow/artifacts.ts +18 -0
  322. package/workflow/executor.test.ts +1 -12
  323. package/workflow/executor.ts +101 -110
  324. package/workflow/index.ts +0 -1
  325. package/workflow/moduleIndex.test.ts +4 -15
  326. package/workflow/moduleIndex.ts +9 -49
  327. package/workflow/plan.ts +18 -25
  328. package/workflow/render.ts +80 -96
  329. package/workflow/rolloutGate.test.ts +1 -1
  330. package/workflow/source.ts +52 -69
  331. package/workflow/sourceAst.ts +50 -0
  332. package/workspaceLayout.test.ts +18 -3
  333. package/workspaceLayout.ts +22 -23
  334. package/abstractCompactor.ts +0 -112
  335. package/aiEditor.test.ts +0 -60
  336. package/aiEditor.ts +0 -387
  337. package/applicationReleasePackager.ts +0 -209
  338. package/capacitorApp.test.ts +0 -372
  339. package/capacitorApp.ts +0 -1648
  340. package/commandDecorators/commandMeta.ts +0 -7
  341. package/commandDecorators/types.ts +0 -10
  342. package/getDirname.ts +0 -4
  343. package/guideline.ts +0 -19
  344. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  345. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  346. package/workflow/primitive.ts +0 -59
@@ -4,24 +4,14 @@ import { existsSync } from "node:fs";
4
4
  import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { Linter } from "../linter";
7
8
 
8
- /**
9
- * Fixture tests for the GritQL plugins in this folder.
10
- *
11
- * These rules break the apps/libs build, and a rule that stops matching reports **zero diagnostics** rather
12
- * than an error — so a biome upgrade can retire one silently. GritQL has known sharp edges here (escaped
13
- * quotes, argument-position matching), which is what makes the quiet failure plausible rather than
14
- * theoretical.
15
- *
16
- * Each rule runs alone, against a config this test writes, so a diagnostic can only have come from that
17
- * plugin. The path scoping in `biome.base.json` is deliberately not exercised — what is under test is
18
- * whether the pattern still matches. A rule that reads `$filename` needs a real path anyway, which is what
19
- * a case's `fixture.json` supplies.
20
- */
9
+ //? A rule that stops matching reports zero diagnostics, not an error, so a biome upgrade can retire one silently.
10
+ //? Each rule runs alone against a config written here; the path scoping in biome.base.json is deliberately not tested.
21
11
  const lintDir = import.meta.dir;
22
12
  const fixturesDir = path.join(lintDir, "__fixtures__");
23
13
  const workspaceRoot = path.resolve(lintDir, "../../../..");
24
- const biomeBin = path.join(workspaceRoot, "node_modules/.bin/biome");
14
+ const biomeBin = path.join(workspaceRoot, "node_modules", ".bin", Linter.biomeBinName);
25
15
 
26
16
  interface BiomeDiagnostic {
27
17
  severity: string;
@@ -30,13 +20,10 @@ interface BiomeDiagnostic {
30
20
  location?: { start?: { line?: number } };
31
21
  }
32
22
 
33
- /**
34
- * `path` is where the source is written inside the temp workspace, for a rule that gates on `$filename`;
35
- * its extension also decides whether the file may hold JSX. `expect: "file"` is for a rule whose span is
36
- * the whole module (`JsModule()`), where there is no per-line diagnostic to match.
37
- */
38
23
  interface FixtureMeta {
24
+ /** Where the source is written, for a rule that gates on `$filename`; its extension decides whether JSX parses. */
39
25
  path?: string;
26
+ /** `"file"`: the rule's span is the whole module (`JsModule()`), so there is no per-line diagnostic to match. */
40
27
  expect?: "lines" | "file";
41
28
  }
42
29
 
@@ -64,7 +51,6 @@ const runBiome = (args: string[], cwd: string) =>
64
51
  proc.on("close", () => resolve(stdout || stderr));
65
52
  });
66
53
 
67
- /** Every directory holding a `bad.*`, named `<rule>` or `<rule>/<case>`. */
68
54
  const collectCases = async (): Promise<FixtureCase[]> => {
69
55
  const cases: FixtureCase[] = [];
70
56
  const walk = async (dir: string, rule: string, name: string) => {
@@ -92,7 +78,6 @@ const sourceName = (fixture: FixtureCase, kind: "bad" | "good") => {
92
78
  const readFixture = async (fixture: FixtureCase, kind: "bad" | "good") =>
93
79
  await readFile(path.join(fixture.dir, sourceName(fixture, kind).read), "utf-8");
94
80
 
95
- /** Diagnostics from one plugin against one fixture, keyed by the 1-based line each was reported on. */
96
81
  const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
97
82
  const dir = await mkdtemp(path.join(os.tmpdir(), `grit-${fixture.rule}-`));
98
83
  tempDirs.push(dir);
@@ -127,7 +112,7 @@ const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
127
112
  };
128
113
  };
129
114
 
130
- /** The 1-based line of every line carrying `marker`. One case per line is the fixture contract. */
115
+ //? One case per line is the fixture contract.
131
116
  const markedLines = async (fixture: FixtureCase, kind: "bad" | "good", marker: string) =>
132
117
  (await readFixture(fixture, kind))
133
118
  .split("\n")
@@ -164,8 +149,7 @@ describe("grit lint rules", () => {
164
149
  }
165
150
  const expected = await markedLines(fixture, "bad", "// @flag");
166
151
  expect(expected.length).toBeGreaterThan(0);
167
- // Set equality, not containment: a marked line nobody reported is a pattern that stopped matching,
168
- // and a reported line nobody marked is the rule reaching further than the fixture claims.
152
+ // Set equality: an unreported mark is a pattern that stopped matching, an unmarked report is overreach.
169
153
  expect(lines).toEqual(expected);
170
154
  });
171
155
 
@@ -15,7 +15,7 @@ or {
15
15
  JsModuleSource() as $source where {
16
16
  $source <: within JsImport(),
17
17
  not $filename <: r".*\.(?:test|spec)\.tsx?",
18
- $filename <: r".*(?:apps|libs)/[^/]+/lib/[^/]+/[^/]+\.(?:ts|tsx)",
18
+ $filename <: r".*(?:apps|libs)[/\\][^/\\]+[/\\]lib[/\\][^/\\]+[/\\][^/\\]+\.(?:ts|tsx)",
19
19
  $source <: r"\"\.\./\.\./.*\"",
20
20
  register_diagnostic(
21
21
  span = $source,
@@ -25,7 +25,7 @@ or {
25
25
  JsModuleSource() as $source where {
26
26
  $source <: within JsImport(),
27
27
  not $filename <: r".*\.(?:test|spec)\.tsx?",
28
- $filename <: r".*/lib/.+\.tsx",
28
+ $filename <: r".*[/\\]lib[/\\].+\.tsx",
29
29
  $source <: r"\"\.\./.*\"",
30
30
  register_diagnostic(
31
31
  span = $source,
@@ -0,0 +1,29 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // A native shell serves the page from app://localhost on iOS, macOS and Linux, a custom scheme WebKit keeps no
5
+ // cookie jar for: `document.cookie` reads "" and a write is dropped without an error, so the value exists in the
6
+ // browser and silently vanishes in the app. akanjs/client's cookie helpers keep an app:// page's cookies in a
7
+ // storage-backed jar, which is the only spelling that works in every shell.
8
+ //
9
+ // Pinned to the member nodes: a bare snippet also matches JSX text that spells the same words (a docs page), and a
10
+ // write is its own node (JsStaticMemberAssignment) that the expression form does not match.
11
+ or {
12
+ JsStaticMemberExpression() as $access where {
13
+ $access <: or { `document.cookie`, `window.document.cookie`, `globalThis.document.cookie`, `self.document.cookie` },
14
+ register_diagnostic(
15
+ span = $access,
16
+ message = "`document.cookie` is always empty on a native app page (app://localhost keeps no cookies), so this value vanishes in the app. Use `getCookie` / `setCookie` / `removeCookie` from `akanjs/client`.",
17
+ severity = "error"
18
+ )
19
+ },
20
+ JsStaticMemberAssignment(object = $object, member = $member) as $access where {
21
+ $member <: `cookie`,
22
+ $object <: or { `document`, `window.document`, `globalThis.document`, `self.document` },
23
+ register_diagnostic(
24
+ span = $access,
25
+ message = "`document.cookie` is always empty on a native app page (app://localhost keeps no cookies), so this value vanishes in the app. Use `getCookie` / `setCookie` / `removeCookie` from `akanjs/client`.",
26
+ severity = "error"
27
+ )
28
+ }
29
+ }
@@ -14,7 +14,8 @@ language js(typescript, jsx)
14
14
  $base <: contains `endpoint`,
15
15
  $className <: r"([A-Z][a-zA-Z0-9]*)Endpoint"($Cap)
16
16
  },
17
- $filename <: r".*/([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
17
+ // `$filename` carries backslashes on Windows, so a separator here must accept both.
18
+ $filename <: r".*[/\\]([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
18
19
  or {
19
20
  $key <: $model,
20
21
  $key <: r"(?:light|create|update|remove|view|edit|merge)([A-Z][a-zA-Z0-9]*)"($keyCap) where {
@@ -0,0 +1,27 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // `via(XObject, LightX, ...)` mixes in only the prototypes of XObject and LightX (applyMixins in
5
+ // akanjs/common), so a static declared on either never reaches the full model `X`. Statics belong on
6
+ // XInput (or a scalar), X, or XInsight. The Object class is recognised by its `*Object` name, because its
7
+ // via() shape is identical to the Insight's; the Light class also by its `[...] as const` field tuple.
8
+ or {
9
+ JsMethodClassMember(),
10
+ JsPropertyClassMember(),
11
+ JsGetterClassMember(),
12
+ JsSetterClassMember(),
13
+ JsStaticInitializationBlockClassMember()
14
+ } as $member where {
15
+ $member <: r"(?:(?:public|protected|private|declare|override|readonly|accessor)\s+)*static\b[\s\S]*",
16
+ $member <: within JsClassDeclaration() as $model where {
17
+ $model <: or {
18
+ r"class\s+(?:Light[A-Za-z0-9_$]*|[A-Za-z0-9_$]*Object)\s+extends\s+via\([\s\S]*",
19
+ r"class\s+[A-Za-z0-9_$]+\s+extends\s+via\(\s*[A-Za-z0-9_$.]+\s*,\s*\[[\s\S]*"
20
+ }
21
+ },
22
+ register_diagnostic(
23
+ span = $member,
24
+ message = "A static on an XObject or LightX class never reaches the full model, because via() mixes in their prototypes only. Declare it on XInput, the full model X, or XInsight instead.",
25
+ severity = "error"
26
+ )
27
+ }
@@ -0,0 +1,44 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Browser APIs a native shell's WebView lacks: Android's WebView has no Web Share, an app:// page on iOS has no
5
+ // service worker, no WebView has the Notification API, and iOS has no vibration. They stay legal on the web, so the
6
+ // rule does not ban them — it keeps them in `webkit/`, the layer whose hooks branch on the platform (and is
7
+ // excluded from this rule's scope in biome.base.json), and names the native side where one exists.
8
+ //
9
+ // Pinned to member access, so JSX text naming an API does not match; `Notification` only through its static entry
10
+ // points, because the name is also the libs/shared model (`Notification.View`, `cnst.Notification`).
11
+ or {
12
+ JsStaticMemberExpression() as $access where {
13
+ $access <: or { `navigator.share`, `navigator.canShare` },
14
+ register_diagnostic(
15
+ span = $access,
16
+ message = "Android's WebView has no Web Share, so this does nothing in the app. Move it into a `webkit/` hook that branches on `isNativeApp()` from `akanjs/client/native`; the runtime's `share` plugin is the app side.",
17
+ severity = "warn"
18
+ )
19
+ },
20
+ JsStaticMemberExpression() as $access where {
21
+ $access <: or { `navigator.serviceWorker`, `Notification.requestPermission`, `Notification.permission` },
22
+ register_diagnostic(
23
+ span = $access,
24
+ message = "A native app page has no service worker or Notification API. Keep this in a `webkit/` hook behind `isNativeApp()`; native push is `push` from `akanjs/client/native`, and `usePushNotification` in libs/util wraps both.",
25
+ severity = "warn"
26
+ )
27
+ },
28
+ JsStaticMemberExpression() as $access where {
29
+ $access <: `navigator.geolocation`,
30
+ register_diagnostic(
31
+ span = $access,
32
+ message = "`navigator.geolocation` skips the app's location plugin and its permission flow. Use `useGeoLocation` from `akanjs/webkit`.",
33
+ severity = "warn"
34
+ )
35
+ },
36
+ JsStaticMemberExpression() as $access where {
37
+ $access <: `navigator.vibrate`,
38
+ register_diagnostic(
39
+ span = $access,
40
+ message = "iOS has no `navigator.vibrate`. Use `haptics` from `akanjs/client/native` in a `webkit/` hook.",
41
+ severity = "warn"
42
+ )
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Web Storage works inside the native shells, so this is not about a crash: akanjs/client's `storage` is where the
5
+ // backend is chosen per platform (the shell's Preferences — UserDefaults, SharedPreferences, a JSON file on the
6
+ // desktop — in an app, localStorage on the web), `secretStorage` keeps credentials in the Keychain/Keystore and
7
+ // clears them on a reinstall, and both answer nothing on the server where a bare `localStorage` read during SSR
8
+ // throws.
9
+ //
10
+ // Only the global is matched — a reference, or a member of window/globalThis/self — pinned to those node kinds: a
11
+ // bare snippet matches the name in any position, a field, an object key or JSX text included.
12
+ or {
13
+ JsIdentifierExpression() as $access where {
14
+ $access <: or { `localStorage`, `sessionStorage` },
15
+ register_diagnostic(
16
+ span = $access,
17
+ message = "Web Storage bypasses the store akanjs picks per platform (the shell's Preferences in an app) and throws during SSR. Use `storage` from `akanjs/client`, or `secretStorage` for tokens and credentials; a value that may be lost with the tab belongs in component state or the store.",
18
+ severity = "error"
19
+ )
20
+ },
21
+ JsStaticMemberExpression(object = $root, member = $name) as $access where {
22
+ $root <: or { `window`, `globalThis`, `self` },
23
+ $name <: or { `localStorage`, `sessionStorage` },
24
+ register_diagnostic(
25
+ span = $access,
26
+ message = "Web Storage bypasses the store akanjs picks per platform (the shell's Preferences in an app) and throws during SSR. Use `storage` from `akanjs/client`, or `secretStorage` for tokens and credentials; a value that may be lost with the tab belongs in component state or the store.",
27
+ severity = "error"
28
+ )
29
+ }
30
+ }
package/linter.test.ts CHANGED
@@ -27,8 +27,7 @@ describe("parseBiomeReport", () => {
27
27
  });
28
28
 
29
29
  test("reads a report behind leading text that carries a brace", () => {
30
- // The whole reason the first-brace-to-last-brace slice was wrong: Biome prints configuration and IO
31
- // diagnostics onto the same stream as the report, and those messages quote source and config.
30
+ // Biome prints configuration and IO diagnostics onto the report's stream, and they quote source and config.
32
31
  const output = [
33
32
  "configuration/deserialize: unknown key `overrides[0].includes { }`",
34
33
  " the file `biome.jsonc` cannot be read",
package/linter.ts CHANGED
@@ -58,16 +58,8 @@ interface LintResponse {
58
58
  warnings: LintMessage[];
59
59
  }
60
60
 
61
- /**
62
- * Extracts the `--reporter=json` report from Biome's output.
63
- *
64
- * Biome mixes configuration and IO diagnostics into the same stream as the report, so the output is not
65
- * always JSON alone. Slicing from the first `{` to the last `}` mis-parses as soon as that leading text
66
- * carries a brace of its own, which is not hypothetical: a config error printed ahead of the report made
67
- * one unchanged command alternate between 2 and 10 diagnostics. So every `{` is tried as a start, its
68
- * match is found by depth while string literals are skipped, and the first candidate that both parses and
69
- * carries a report field wins — a brace in prose parses as nothing, and `{"a":1}` in prose is not a report.
70
- */
61
+ // Biome mixes config and IO diagnostics into the report stream, and that text can carry braces of its own, so every
62
+ // `{` is tried as a start (string literals skipped) and the first candidate that parses and carries a report field wins.
71
63
  export const parseBiomeReport = (output: string): BiomeReport => {
72
64
  for (let start = output.indexOf("{"); start !== -1; start = output.indexOf("{", start + 1)) {
73
65
  const end = objectEndAt(output, start);
@@ -111,7 +103,19 @@ const asBiomeReport = (candidate: string): BiomeReport | null => {
111
103
  /** Biome reads `biome.json` first and `biome.jsonc` second; only the latter may carry comments. */
112
104
  const BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"] as const;
113
105
 
106
+ const emptyResult = (filePath: string): LintResult => ({
107
+ filePath,
108
+ messages: [],
109
+ errorCount: 0,
110
+ warningCount: 0,
111
+ fixableErrorCount: 0,
112
+ fixableWarningCount: 0,
113
+ });
114
+
114
115
  export class Linter {
116
+ //? Bun links a package bin as `<name>.exe` (plus a `.bunx` stub) on Windows, and a bare `<name>` elsewhere.
117
+ static readonly biomeBinName = process.platform === "win32" ? "biome.exe" : "biome";
118
+
115
119
  lintRoot: string;
116
120
  configPath: string;
117
121
  #biomeBin: string;
@@ -119,7 +123,7 @@ export class Linter {
119
123
  constructor(cwdPath: string) {
120
124
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
121
125
  this.configPath = Linter.#configPathIn(this.lintRoot) ?? path.join(this.lintRoot, "biome.json");
122
- const localBiomeBin = path.join(this.lintRoot, "node_modules/.bin/biome");
126
+ const localBiomeBin = path.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
123
127
  this.#biomeBin = existsSync(localBiomeBin) ? localBiomeBin : "biome";
124
128
  }
125
129
 
@@ -199,16 +203,7 @@ export class Linter {
199
203
  for (const diagnostic of report.diagnostics ?? []) {
200
204
  if (diagnostic.severity !== "error" && diagnostic.severity !== "warning") continue;
201
205
  const diagnosticFilePath = this.#diagnosticFilePath(diagnostic, filePath);
202
- const result =
203
- resultsByPath.get(diagnosticFilePath) ??
204
- ({
205
- filePath: diagnosticFilePath,
206
- messages: [],
207
- errorCount: 0,
208
- warningCount: 0,
209
- fixableErrorCount: 0,
210
- fixableWarningCount: 0,
211
- } satisfies LintResult);
206
+ const result = resultsByPath.get(diagnosticFilePath) ?? emptyResult(diagnosticFilePath);
212
207
  const message = this.#createLintMessage(diagnostic);
213
208
  result.messages.push(message);
214
209
  if (message.severity === 2) result.errorCount += 1;
@@ -217,15 +212,7 @@ export class Linter {
217
212
  }
218
213
 
219
214
  return [
220
- resultsByPath.get(filePath) ??
221
- ({
222
- filePath,
223
- messages: [],
224
- errorCount: 0,
225
- warningCount: 0,
226
- fixableErrorCount: 0,
227
- fixableWarningCount: 0,
228
- } satisfies LintResult),
215
+ resultsByPath.get(filePath) ?? emptyResult(filePath),
229
216
  ...[...resultsByPath.entries()].filter(([resultPath]) => resultPath !== filePath).map(([, result]) => result),
230
217
  ];
231
218
  }
@@ -269,22 +256,12 @@ export class Linter {
269
256
  return await this.lintFile(filePath);
270
257
  }
271
258
 
272
- /**
273
- * Lint a single file using Biome.
274
- * @param filePath - Path to the file to lint
275
- * @returns Array of Biome results in the legacy lint result shape
276
- */
277
259
  async lintFile(filePath: string): Promise<LintResponse> {
278
260
  const resolvedFilePath = this.#resolveFilePath(filePath);
279
261
  if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
280
262
  return await this.#checkFile(resolvedFilePath);
281
263
  }
282
264
 
283
- /**
284
- * Format lint results for console output
285
- * @param results - Array of Biome results
286
- * @returns Formatted string
287
- */
288
265
  formatLintResults(results: LintResult[]): string {
289
266
  if (results.length === 0) return "No files to lint";
290
267
 
@@ -305,7 +282,7 @@ export class Linter {
305
282
  const sourceContent = readFileSync(result.filePath, "utf8");
306
283
  sourceLines = sourceContent.split("\n");
307
284
  } catch {
308
- // Ignore read errors
285
+ //? An unreadable source only loses its code frame.
309
286
  }
310
287
  }
311
288
 
@@ -318,14 +295,12 @@ export class Linter {
318
295
  output.push(`\n ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`);
319
296
  output.push(` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`);
320
297
 
321
- // Show source line with underline
322
298
  if (sourceLines.length > 0 && message.line <= sourceLines.length) {
323
299
  const sourceLine = sourceLines[message.line - 1];
324
300
  const lineNumber = message.line.toString().padStart(5, " ");
325
301
 
326
302
  output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
327
303
 
328
- // Create underline
329
304
  const underlinePrefix = " ".repeat(message.column - 1);
330
305
  const underlineLength = message.endColumn ? message.endColumn - message.column : 1;
331
306
  const underline = "^".repeat(Math.max(1, underlineLength));
@@ -345,27 +320,7 @@ export class Linter {
345
320
  return summary.concat(output).join("\n");
346
321
  }
347
322
 
348
- /**
349
- * Get detailed lint information
350
- * @param filePath - Path to the file to lint
351
- * @returns Object containing detailed lint information
352
- */
353
- async getDetailedLintInfo(filePath: string): Promise<{
354
- results: LintResult[];
355
- details: {
356
- line: number;
357
- column: number;
358
- message: string;
359
- ruleId: string | null;
360
- severity: "error" | "warning";
361
- }[];
362
- stats: {
363
- errorCount: number;
364
- warningCount: number;
365
- fixableErrorCount: number;
366
- fixableWarningCount: number;
367
- };
368
- }> {
323
+ async getDetailedLintInfo(filePath: string) {
369
324
  const { results } = await this.lintFile(filePath);
370
325
 
371
326
  const details = results.flatMap((result) =>
@@ -385,22 +340,13 @@ export class Linter {
385
340
  fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
386
341
  fixableWarningCount: acc.fixableWarningCount + result.fixableWarningCount,
387
342
  }),
388
- {
389
- errorCount: 0,
390
- warningCount: 0,
391
- fixableErrorCount: 0,
392
- fixableWarningCount: 0,
393
- },
343
+ { errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 },
394
344
  );
395
345
 
396
346
  return { results, details, stats };
397
347
  }
398
348
 
399
- /**
400
- * Check if a file has lint errors
401
- * @param filePath - Path to the file to check
402
- * @returns true if there are no errors, false otherwise
403
- */
349
+ /** Also false when the file cannot be linted at all. */
404
350
  async hasNoLintErrors(filePath: string): Promise<boolean> {
405
351
  try {
406
352
  const { results } = await this.lintFile(filePath);
@@ -410,32 +356,17 @@ export class Linter {
410
356
  }
411
357
  }
412
358
 
413
- /**
414
- * Get only error messages (excluding warnings)
415
- * @param filePath - Path to the file to lint
416
- * @returns Array of error messages
417
- */
418
359
  async getErrors(filePath: string): Promise<LintMessage[]> {
419
360
  const { results } = await this.lintFile(filePath);
420
361
  return results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
421
362
  }
422
363
 
423
- /**
424
- * Get only warning messages
425
- * @param filePath - Path to the file to lint
426
- * @returns Array of warning messages
427
- */
428
364
  async getWarnings(filePath: string): Promise<LintMessage[]> {
429
365
  const { results } = await this.lintFile(filePath);
430
366
  return results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
431
367
  }
432
368
 
433
- /**
434
- * Fix lint errors automatically
435
- * @param filePath - Path to the file to fix
436
- * @param dryRun - If true, returns the fixed content without writing to file
437
- * @returns Fixed content and remaining issues
438
- */
369
+ /** `dryRun` returns the fixed content in `output` without writing the file. */
439
370
  async fixFile(filePath: string, dryRun = false): Promise<LintResponse> {
440
371
  const resolvedFilePath = this.#resolveFilePath(filePath);
441
372
  if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
@@ -451,19 +382,8 @@ export class Linter {
451
382
  return { ...lintResult, fixed: stdout !== source, output: stdout };
452
383
  }
453
384
 
454
- /**
455
- * Get Biome configuration for a file
456
- * @param filePath - Path to the file
457
- * @returns Biome configuration object
458
- */
459
- /**
460
- * Applies Biome's safe fixes to many files in one process.
461
- *
462
- * One spawn, not one per file: Biome's startup dominates a small file, so the per-file path turns a
463
- * fifteen-file scaffold into three seconds of process launches. Paths that do not exist are dropped
464
- * rather than thrown on, because the caller is usually reporting what a template just wrote and a
465
- * template is allowed to decline a file.
466
- */
385
+ // One spawn for all files: Biome's startup dominates a small file. Missing paths are dropped (a template may
386
+ // decline a file).
467
387
  async fixFiles(filePaths: string[]): Promise<{ fixed: string[] }> {
468
388
  const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync);
469
389
  if (resolved.length === 0) return { fixed: [] };
@@ -489,21 +409,11 @@ export class Linter {
489
409
  return JSON.parse(readFileSync(this.configPath, "utf8")) as unknown;
490
410
  }
491
411
 
492
- /**
493
- * Get rules that are causing errors in a file
494
- * @param filePath - Path to the file to check
495
- * @returns Object mapping rule IDs to their error counts
496
- */
497
412
  async getProblematicRules(filePath: string): Promise<Record<string, number>> {
498
413
  const { results } = await this.lintFile(filePath);
499
414
  const ruleCounts: Record<string, number> = {};
500
-
501
- results.forEach((result) => {
502
- result.messages.forEach((message) => {
503
- if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
504
- });
505
- });
506
-
415
+ for (const message of results.flatMap((result) => result.messages))
416
+ if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
507
417
  return ruleCounts;
508
418
  }
509
419
  }