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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +3 -2
  3. package/abstractDoc.test.ts +6 -15
  4. package/abstractDoc.ts +6 -8
  5. package/agentsIndex.test.ts +4 -7
  6. package/agentsIndex.ts +6 -22
  7. package/akanApp/BackendImportGraph.test.ts +8 -22
  8. package/akanApp/BackendImportGraph.ts +1 -20
  9. package/akanApp/akanApp.host.test.ts +1 -3
  10. package/akanApp/akanApp.host.ts +82 -254
  11. package/akanApp/devHostPolicy.test.ts +10 -34
  12. package/akanApp/devHostPolicy.ts +12 -98
  13. package/akanConfig/akanConfig.test.ts +142 -106
  14. package/akanConfig/akanConfig.ts +147 -191
  15. package/akanConfig/types.ts +4 -3
  16. package/akanContext.ts +110 -160
  17. package/akanMcpContract.ts +28 -73
  18. package/appSelectionMemory.ts +1 -4
  19. package/applicationBuildReporter.ts +37 -5
  20. package/applicationBuildRunner.test.ts +5 -3
  21. package/applicationBuildRunner.ts +54 -61
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +32 -30
  24. package/artifact/implicitRootLayout.ts +58 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +32 -1
  28. package/biomeBase.ts +2 -5
  29. package/cloud/cloudApi.test.ts +202 -0
  30. package/cloud/cloudApi.ts +77 -39
  31. package/cloud/configLock.test.ts +64 -0
  32. package/cloud/configLock.ts +96 -0
  33. package/cloud/constants.ts +26 -5
  34. package/cloud/globalConfig.ts +70 -32
  35. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  36. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  37. package/codeAgent/agent/CodeAgent.ts +551 -0
  38. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  39. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  40. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  41. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  42. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  43. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  44. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  45. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  46. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  47. package/codeAgent/agent/CodeMailbox.ts +148 -0
  48. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  49. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  50. package/codeAgent/agent/SubagentPool.ts +214 -0
  51. package/codeAgent/agent/akanCodeModel.ts +47 -0
  52. package/codeAgent/agent/akanCodePaths.ts +31 -0
  53. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  54. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  55. package/codeAgent/agent/codeAgent.test.ts +178 -0
  56. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  57. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  58. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  59. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  60. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  61. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  62. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  63. package/codeAgent/agent/consoleToStderr.ts +8 -0
  64. package/codeAgent/agent/jsonLines.ts +29 -0
  65. package/codeAgent/agent/stringArg.ts +5 -0
  66. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  67. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  68. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  69. package/codeAgent/feedback/PreviewView.ts +95 -0
  70. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  71. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  72. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  73. package/codeAgent/index.ts +34 -0
  74. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  75. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  76. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  77. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  78. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  79. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  80. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  81. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  82. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  83. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  84. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  85. package/codeAgent/tools/AkanCli.ts +50 -0
  86. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  87. package/codeAgent/tools/AkanEditScope.ts +48 -0
  88. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  89. package/codeAgent/tools/AkanToolPack.ts +92 -0
  90. package/codeAgent/tools/AkanVerifier.ts +48 -0
  91. package/codeAgent/tools/AskToolPack.ts +99 -0
  92. package/codeAgent/tools/MailToolPack.ts +76 -0
  93. package/codeAgent/tools/McpClient.ts +187 -0
  94. package/codeAgent/tools/McpOAuth.ts +233 -0
  95. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  96. package/codeAgent/tools/McpServerConfig.ts +129 -0
  97. package/codeAgent/tools/McpSignIn.ts +158 -0
  98. package/codeAgent/tools/McpTokenStore.ts +81 -0
  99. package/codeAgent/tools/McpToolPack.ts +199 -0
  100. package/codeAgent/tools/SessionToolPack.ts +91 -0
  101. package/codeAgent/tools/WebToolPack.ts +147 -0
  102. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  103. package/codeAgent/tools/askToolPack.test.ts +116 -0
  104. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  105. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  106. package/codegenLock.test.ts +4 -13
  107. package/codegenLock.ts +3 -17
  108. package/commandDecorators/argMeta.ts +1 -5
  109. package/commandDecorators/camelToKebabCase.ts +1 -0
  110. package/commandDecorators/command.ts +46 -102
  111. package/commandDecorators/commandBuilder.ts +30 -61
  112. package/commandDecorators/commandDecorators.test.ts +2 -10
  113. package/commandDecorators/dependencyBuilder.ts +10 -1
  114. package/commandDecorators/helpFormatter.ts +68 -145
  115. package/commandDecorators/index.ts +0 -2
  116. package/commandDecorators/targetMeta.ts +9 -7
  117. package/createTunnel.ts +7 -14
  118. package/dependencyScanner.test.ts +4 -16
  119. package/dependencyScanner.ts +31 -80
  120. package/devkitUtils.test.ts +48 -20
  121. package/executors.linkFallback.test.ts +103 -0
  122. package/executors.test.ts +132 -106
  123. package/executors.ts +195 -496
  124. package/fileEditor.ts +9 -37
  125. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  126. package/fileSys.ts +6 -8
  127. package/formSetterScanner.ts +3 -16
  128. package/frontendBuild/allRoutesBuilder.ts +7 -21
  129. package/frontendBuild/autoImportSync.test.ts +27 -38
  130. package/frontendBuild/autoImportSync.ts +31 -65
  131. package/frontendBuild/buildRouteClient.test.ts +62 -37
  132. package/frontendBuild/bundleDefine.ts +12 -0
  133. package/frontendBuild/clientBuildTypes.ts +5 -36
  134. package/frontendBuild/clientEntriesBundler.ts +9 -40
  135. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  136. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  137. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  138. package/frontendBuild/cssCandidateCache.ts +6 -41
  139. package/frontendBuild/cssCompiler.ts +29 -78
  140. package/frontendBuild/cssImportResolver.ts +6 -19
  141. package/frontendBuild/devChangePlanner.ts +7 -10
  142. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  143. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  144. package/frontendBuild/fontOptimizer.test.ts +6 -14
  145. package/frontendBuild/fontOptimizer.ts +15 -72
  146. package/frontendBuild/fontPruner.test.ts +5 -10
  147. package/frontendBuild/fontPruner.ts +5 -20
  148. package/frontendBuild/frontendBuild.test.ts +46 -69
  149. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  150. package/frontendBuild/hmrWatcher.test.ts +8 -32
  151. package/frontendBuild/hmrWatcher.ts +14 -71
  152. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  153. package/frontendBuild/precompressArtifacts.ts +5 -18
  154. package/frontendBuild/routeClientBuilder.ts +23 -46
  155. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  156. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  157. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  158. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  159. package/frontendBuild/styleContract.ts +1 -8
  160. package/frontendBuild/themeValidator.ts +4 -26
  161. package/frontendBuild/watchRootResolver.test.ts +23 -42
  162. package/frontendBuild/watchRootResolver.ts +5 -27
  163. package/getRelatedCnsts.ts +12 -92
  164. package/incrementalBuilder/buildBatch.proc.ts +29 -58
  165. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  166. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  167. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  168. package/incrementalBuilder/builderChannel.test.ts +24 -49
  169. package/incrementalBuilder/builderChannel.ts +6 -37
  170. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  171. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  172. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  173. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  174. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  175. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  176. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  177. package/index.ts +5 -19
  178. package/integration/devResourceProbe.ts +50 -56
  179. package/integration/devStability.integration.test.ts +47 -194
  180. package/integration/devStabilityHarness.test.ts +13 -28
  181. package/integration/devStabilityHarness.ts +65 -330
  182. package/integration/ssrMemoryProbe.ts +41 -167
  183. package/libSource.test.ts +4 -15
  184. package/libSource.ts +19 -31
  185. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  186. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  187. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  188. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  189. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  190. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  191. package/lint/gritRules.test.ts +8 -24
  192. package/lint/no-deep-internal-import.grit +2 -2
  193. package/lint/no-document-cookie.grit +29 -0
  194. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  195. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  196. package/lint/no-web-storage.grit +30 -0
  197. package/linter.test.ts +1 -2
  198. package/linter.ts +26 -116
  199. package/mobile/index.ts +6 -0
  200. package/mobile/mobileTarget.ts +16 -13
  201. package/mobile/nativeApi.test.ts +30 -0
  202. package/mobile/nativeApi.ts +40 -0
  203. package/mobile/nativeApp.test.ts +85 -0
  204. package/mobile/nativeApp.ts +210 -0
  205. package/mobile/nativeConfig.test.ts +199 -0
  206. package/mobile/nativeConfig.ts +184 -0
  207. package/mobile/nativeWebDir.test.ts +80 -0
  208. package/mobile/nativeWebDir.ts +62 -0
  209. package/mobile/placeholderAppId.test.ts +13 -0
  210. package/mobile/placeholderAppId.ts +16 -0
  211. package/mobile/usageDescriptions.test.ts +27 -0
  212. package/mobile/usageDescriptions.ts +24 -0
  213. package/package.json +10 -10
  214. package/packageExports.test.ts +3 -18
  215. package/packageExportsMap.ts +3 -19
  216. package/piDependencyPins.test.ts +46 -0
  217. package/platformTest/BunTestReport.test.ts +60 -0
  218. package/platformTest/BunTestReport.ts +65 -0
  219. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  220. package/platformTest/IdleWatchedProcess.ts +73 -0
  221. package/platformTest/LinuxDockerTarget.ts +105 -0
  222. package/platformTest/PlatformTestRun.test.ts +64 -0
  223. package/platformTest/PlatformTestRun.ts +141 -0
  224. package/platformTest/PlatformTestTarget.ts +142 -0
  225. package/platformTest/TestSnapshot.test.ts +72 -0
  226. package/platformTest/TestSnapshot.ts +103 -0
  227. package/platformTest/WindowsSshTarget.ts +151 -0
  228. package/prompter.ts +3 -13
  229. package/qualityScanner.test.ts +9 -26
  230. package/qualityScanner.ts +200 -286
  231. package/recipeScanner.test.ts +7 -12
  232. package/recipeScanner.ts +9 -38
  233. package/repoIdentity.ts +2 -9
  234. package/routeSourceValidator.test.ts +0 -1
  235. package/routeSourceValidator.ts +7 -42
  236. package/scanInfo.ts +91 -225
  237. package/selectModel.ts +1 -2
  238. package/semver.ts +3 -5
  239. package/slicePlanner.test.ts +6 -24
  240. package/slicePlanner.ts +11 -32
  241. package/spinner.test.ts +3 -8
  242. package/spinner.ts +4 -16
  243. package/sshTunnel.ts +1 -6
  244. package/ssrScanner.test.ts +1 -6
  245. package/ssrScanner.ts +103 -113
  246. package/stripAnsi.ts +3 -0
  247. package/subspace.test.ts +30 -25
  248. package/subspace.ts +81 -115
  249. package/subspaceConfig.ts +11 -9
  250. package/testHelpers.ts +194 -0
  251. package/transforms/asyncDefaultExportDetector.ts +3 -11
  252. package/transforms/barrelAnalyzer.ts +43 -135
  253. package/transforms/barrelImportsPlugin.ts +38 -175
  254. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  255. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  256. package/transforms/moduleSyntax.ts +32 -0
  257. package/transforms/rscUseClientTransform.ts +35 -20
  258. package/transforms/transforms.test.ts +153 -44
  259. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  260. package/transforms/useClientBundlePlugin.ts +2 -22
  261. package/typeChecker.ts +7 -63
  262. package/types.ts +1 -0
  263. package/uploadRelease.ts +10 -25
  264. package/workflow/artifacts.ts +18 -0
  265. package/workflow/executor.test.ts +1 -12
  266. package/workflow/executor.ts +101 -110
  267. package/workflow/index.ts +0 -1
  268. package/workflow/moduleIndex.test.ts +4 -15
  269. package/workflow/moduleIndex.ts +9 -49
  270. package/workflow/plan.ts +18 -25
  271. package/workflow/render.ts +80 -96
  272. package/workflow/rolloutGate.test.ts +1 -1
  273. package/workflow/source.ts +52 -69
  274. package/workflow/sourceAst.ts +50 -0
  275. package/workspaceLayout.test.ts +13 -3
  276. package/workspaceLayout.ts +20 -23
  277. package/abstractCompactor.ts +0 -112
  278. package/aiEditor.test.ts +0 -60
  279. package/aiEditor.ts +0 -387
  280. package/applicationReleasePackager.ts +0 -209
  281. package/capacitorApp.test.ts +0 -372
  282. package/capacitorApp.ts +0 -1648
  283. package/commandDecorators/commandMeta.ts +0 -7
  284. package/commandDecorators/types.ts +0 -10
  285. package/getDirname.ts +0 -4
  286. package/guideline.ts +0 -19
  287. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  288. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  289. package/workflow/primitive.ts +0 -59
package/qualityScanner.ts CHANGED
@@ -6,8 +6,8 @@ import ignore from "ignore";
6
6
  import ts from "typescript";
7
7
  import { AbstractDoc } from "./abstractDoc";
8
8
  import { FormSetterScanner } from "./formSetterScanner";
9
- import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
10
- import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
9
+ import { fileWarning, formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
10
+ import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles, rootEntryHintOf } from "./workspaceLayout";
11
11
 
12
12
  type QualitySeverity = "warning";
13
13
  type QualityScope = "global" | "file" | "convention" | "layout" | "ssr" | "agent";
@@ -103,8 +103,6 @@ const CONVENTION_SUFFIXES = [
103
103
  ".store.ts",
104
104
  ] as const;
105
105
 
106
- // How to remediate each rule, keyed by rule id. Surfaced as a `fix:` line per warning (text + JSON output)
107
- // so the scan result tells the reader what to do, not just what is wrong.
108
106
  const RULE_FIXES: Record<string, string> = {
109
107
  "akan.global.duplicate-exported-function-name":
110
108
  "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
@@ -114,7 +112,7 @@ const RULE_FIXES: Record<string, string> = {
114
112
  "Split the file by responsibility — move Zones, Utils, or subcomponents into sibling files.",
115
113
  "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
116
114
  "akan.file.abstract-max-lines":
117
- "Run `akan compact <app-or-lib>` to rewrite the abstract with the AI editor, keeping only the invariants and workflows the source files cannot show.",
115
+ "Trim the abstract by hand to a title, one sentence naming what the module owns, a `## Rules` list of the invariants the source cannot show, and an optional workflow chain.",
118
116
  "akan.file.placeholder-export":
119
117
  "Remove the placeholder export; generated indexes should only re-export real modules.",
120
118
  "akan.file.bang-comment-in-client":
@@ -297,24 +295,24 @@ export class AkanQualityScanner {
297
295
  const warnings: QualityWarning[] = [];
298
296
  const lineCount = sourceFile.content.split(/\r?\n/).length;
299
297
  const recommendedLineLimit = getRecommendedLineLimit(sourceFile.file);
300
- if (recommendedLineLimit && lineCount > recommendedLineLimit) {
301
- warnings.push({
302
- rule: "akan.file.recommended-max-lines",
303
- scope: "file",
304
- severity: "warning",
305
- file: sourceFile.file,
306
- message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`,
307
- });
308
- }
309
- if (lineCount > MAX_FILE_LINES) {
310
- warnings.push({
311
- rule: "akan.file.max-lines",
312
- scope: "file",
313
- severity: "warning",
314
- file: sourceFile.file,
315
- message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`,
316
- });
317
- }
298
+ if (recommendedLineLimit && lineCount > recommendedLineLimit)
299
+ warnings.push(
300
+ fileWarning(
301
+ "akan.file.recommended-max-lines",
302
+ "file",
303
+ sourceFile.file,
304
+ `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`,
305
+ ),
306
+ );
307
+ if (lineCount > MAX_FILE_LINES)
308
+ warnings.push(
309
+ fileWarning(
310
+ "akan.file.max-lines",
311
+ "file",
312
+ sourceFile.file,
313
+ `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`,
314
+ ),
315
+ );
318
316
 
319
317
  warnings.push(...getBangCommentWarnings(sourceFile));
320
318
  warnings.push(...getPlaceholderExportWarnings(sourceFile));
@@ -327,14 +325,15 @@ export class AkanQualityScanner {
327
325
  for (const declaration of getTopLevelDeclarations(sourceFile)) {
328
326
  if (declaration.kind === "class" && exportedClassNames.includes(declaration.name)) continue;
329
327
  if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name)) continue;
330
- warnings.push({
331
- rule: "akan.file.class-export-global-declaration",
332
- scope: "file",
333
- severity: "warning",
334
- file: sourceFile.file,
335
- line: declaration.line,
336
- message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`,
337
- });
328
+ warnings.push(
329
+ fileWarning(
330
+ "akan.file.class-export-global-declaration",
331
+ "file",
332
+ sourceFile.file,
333
+ `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`,
334
+ declaration.line,
335
+ ),
336
+ );
338
337
  }
339
338
  return warnings;
340
339
  }
@@ -343,9 +342,7 @@ export class AkanQualityScanner {
343
342
  if (!isComponentDeclarationFile(sourceFile.file)) return [];
344
343
  const isPage = isPageRouteFile(sourceFile.file);
345
344
  const declarations = getComponentFileDeclarations(sourceFile.sourceFile);
346
- // Compound/namespaced components (e.g. `Like.WithDislike = WithDislike`) are valid without being exported.
347
345
  const compoundComponentNames = getCompoundComponentNames(sourceFile.sourceFile);
348
- // Components are the exported (or compound) PascalCase values; their "<Component>Props" interface may live here.
349
346
  const componentNames = declarations
350
347
  .filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind))
351
348
  .filter((declaration) => isPascalCaseName(declaration.name))
@@ -358,28 +355,28 @@ export class AkanQualityScanner {
358
355
  if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name)) continue;
359
356
  if (declaration.exported) {
360
357
  if (isAllowedComponentExport(declaration, isPage)) continue;
361
- warnings.push({
362
- rule: "akan.file.component-export",
363
- scope: "file",
364
- severity: "warning",
365
- file: sourceFile.file,
366
- line: declaration.line,
367
- message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`,
368
- });
358
+ warnings.push(
359
+ fileWarning(
360
+ "akan.file.component-export",
361
+ "file",
362
+ sourceFile.file,
363
+ `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`,
364
+ declaration.line,
365
+ ),
366
+ );
369
367
  continue;
370
368
  }
371
- // A non-exported PascalCase component attached as a compound member is an accepted pattern.
372
369
  if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name)) continue;
373
- if (isRestrictedInternalKind(declaration.kind)) {
374
- warnings.push({
375
- rule: "akan.file.component-internal-declaration",
376
- scope: "file",
377
- severity: "warning",
378
- file: sourceFile.file,
379
- line: declaration.line,
380
- message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`,
381
- });
382
- }
370
+ if (isRestrictedInternalKind(declaration.kind))
371
+ warnings.push(
372
+ fileWarning(
373
+ "akan.file.component-internal-declaration",
374
+ "file",
375
+ sourceFile.file,
376
+ `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`,
377
+ declaration.line,
378
+ ),
379
+ );
383
380
  }
384
381
  return warnings;
385
382
  }
@@ -389,35 +386,29 @@ export class AkanQualityScanner {
389
386
  if (!suffix) return [];
390
387
 
391
388
  const modelName = toPascalCase(path.basename(sourceFile.file, suffix));
392
- const warnings: QualityWarning[] = [];
393
- for (const declaration of getTopLevelDeclarations(sourceFile)) {
394
- if (isAllowedConventionDeclaration(suffix, modelName, declaration)) continue;
395
- warnings.push({
396
- rule: `akan.convention${suffix.replace(".ts", "")}`,
397
- scope: "convention",
398
- severity: "warning",
399
- file: sourceFile.file,
400
- line: declaration.line,
401
- message: `${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(
402
- suffix,
403
- modelName,
404
- )}.`,
405
- });
406
- }
407
- return warnings;
389
+ return getTopLevelDeclarations(sourceFile)
390
+ .filter((declaration) => !isAllowedConventionDeclaration(suffix, modelName, declaration))
391
+ .map((declaration) =>
392
+ fileWarning(
393
+ `akan.convention${suffix.replace(".ts", "")}`,
394
+ "convention",
395
+ sourceFile.file,
396
+ `${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`,
397
+ declaration.line,
398
+ ),
399
+ );
408
400
  }
409
401
 
410
402
  #scanAbstractQuality({ file, content }: TextFileInfo): QualityWarning[] {
411
403
  const lineCount = AbstractDoc.lineCountOf(content);
412
404
  if (lineCount <= AbstractDoc.maxLines) return [];
413
405
  return [
414
- {
415
- rule: "akan.file.abstract-max-lines",
416
- scope: "file",
417
- severity: "warning",
406
+ fileWarning(
407
+ "akan.file.abstract-max-lines",
408
+ "file",
418
409
  file,
419
- message: `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`,
420
- },
410
+ `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`,
411
+ ),
421
412
  ];
422
413
  }
423
414
 
@@ -428,27 +419,29 @@ export class AkanQualityScanner {
428
419
  const { type, name, isDir } = rootEntry;
429
420
  const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
430
421
  const kind = isDir ? "folder" : "file";
431
- if (!allowed) {
432
- warnings.push({
433
- rule: `akan.layout.${type}-root-${kind}`,
434
- scope: "layout",
435
- severity: "warning",
436
- file: sourceFile.file,
437
- message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`,
438
- });
439
- }
422
+ if (!allowed)
423
+ warnings.push(
424
+ fileWarning(
425
+ `akan.layout.${type}-root-${kind}`,
426
+ "layout",
427
+ sourceFile.file,
428
+ `Unexpected ${type} root ${kind} "${name}". ${
429
+ rootEntryHintOf(type, name) ?? `Keep ${type} code in conventional ${type} folders.`
430
+ }`,
431
+ ),
432
+ );
440
433
  }
441
434
 
442
435
  const libFacetFile = getLibFacetRootFile(sourceFile.file);
443
- if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
444
- warnings.push({
445
- rule: "akan.layout.lib-facet-file",
446
- scope: "layout",
447
- severity: "warning",
448
- file: sourceFile.file,
449
- message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
450
- });
451
- }
436
+ if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
437
+ warnings.push(
438
+ fileWarning(
439
+ "akan.layout.lib-facet-file",
440
+ "layout",
441
+ sourceFile.file,
442
+ `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
443
+ ),
444
+ );
452
445
 
453
446
  const moduleUiWarning = getModuleUiWarning(sourceFile.file);
454
447
  if (moduleUiWarning) warnings.push(moduleUiWarning);
@@ -517,79 +510,59 @@ function formatQualityLocation(file: string | undefined, line: number | undefine
517
510
 
518
511
  function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionLike[] {
519
512
  const declarations: ExportedFunctionLike[] = [];
520
- // UI component names (e.g. Card, Button) naturally repeat across apps/libs, so exempt ui files from the
521
- // duplicate-exported-name check. This only relaxes the name check; the shared-body check still applies.
513
+ // UI component names (Card, Button) repeat across apps and libs by design; only the name check is relaxed.
522
514
  const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
515
+ const add = (
516
+ name: string,
517
+ kind: ExportedFunctionLike["kind"],
518
+ node: ts.Node,
519
+ body: ts.Node | undefined,
520
+ isEnumClass = false,
521
+ ) =>
522
+ declarations.push({
523
+ name,
524
+ kind,
525
+ file: sourceFile.file,
526
+ line: getLine(sourceFile.sourceFile, node),
527
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
528
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass),
529
+ });
523
530
  for (const statement of sourceFile.sourceFile.statements) {
524
- if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
525
- declarations.push({
526
- name: statement.name.text,
527
- kind: "function",
528
- file: sourceFile.file,
529
- line: getLine(sourceFile.sourceFile, statement),
530
- bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
531
- duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
532
- });
533
- }
534
- if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
535
- declarations.push({
536
- name: statement.name.text,
537
- kind: "class",
538
- file: sourceFile.file,
539
- line: getLine(sourceFile.sourceFile, statement),
540
- bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
541
- duplicateNameExempt:
542
- nameExempt ||
543
- isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement)),
544
- });
545
- }
531
+ if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement))
532
+ add(statement.name.text, "function", statement, statement.body);
533
+ if (ts.isClassDeclaration(statement) && statement.name && isExported(statement))
534
+ add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
546
535
  if (ts.isVariableStatement(statement) && isExported(statement)) {
547
536
  for (const declaration of statement.declarationList.declarations) {
548
537
  if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer)) continue;
549
- declarations.push({
550
- name: declaration.name.text,
551
- kind: "function-variable",
552
- file: sourceFile.file,
553
- line: getLine(sourceFile.sourceFile, declaration),
554
- bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
555
- duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
556
- });
538
+ add(declaration.name.text, "function-variable", declaration, declaration.initializer);
557
539
  }
558
540
  }
559
541
  }
560
542
  return declarations;
561
543
  }
562
544
 
563
- function isPageRouteFile(file: string) {
545
+ function sysSegments(file: string) {
564
546
  const segments = file.split("/");
565
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
547
+ return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
548
+ }
549
+
550
+ function isPageRouteFile(file: string) {
551
+ return sysSegments(file)?.[2] === "page";
566
552
  }
567
553
 
568
554
  function isUiComponentFile(file: string) {
569
- const segments = file.split("/");
570
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
555
+ return sysSegments(file)?.[2] === "ui";
571
556
  }
572
557
 
573
558
  function isConventionDuplicateNameExempt(file: string, isEnumClass: boolean) {
574
- if (!isInLibModule(file)) return false;
575
- if (file.endsWith(".tsx")) return true;
576
- if (
577
- file.endsWith(".document.ts") ||
578
- file.endsWith(".service.ts") ||
579
- file.endsWith(".signal.ts") ||
580
- file.endsWith(".store.ts")
581
- )
582
- return true;
559
+ if (!sysSegments(file)?.includes("lib")) return false;
560
+ if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file)) return true;
583
561
  // Model view classes may repeat across modules; enum classes must stay uniquely named.
584
562
  if (file.endsWith(".constant.ts")) return !isEnumClass;
585
563
  return false;
586
564
  }
587
565
 
588
- function isInLibModule(file: string) {
589
- const segments = file.split("/");
590
- return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
591
- }
592
-
593
566
  function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement) {
594
567
  if (!ts.isClassDeclaration(statement)) return false;
595
568
  const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
@@ -646,9 +619,7 @@ function getComponentFileDeclarations(sourceFile: ts.SourceFile): ComponentFileD
646
619
  return declarations;
647
620
  }
648
621
 
649
- // Detects compound/namespaced component assignments like `Like.WithDislike = WithDislike`. The PascalCase
650
- // member is the public component name; when the right side is a PascalCase identifier it is the local
651
- // definition. Both are treated as valid components so the definition and its "<Component>Props" may stay local.
622
+ // `Like.WithDislike = WithDislike` makes both names components, so the local definition and its Props may stay unexported.
652
623
  function getCompoundComponentNames(sourceFile: ts.SourceFile): Set<string> {
653
624
  const names = new Set<string>();
654
625
  for (const statement of sourceFile.statements) {
@@ -676,7 +647,6 @@ function isAllowedComponentExport(declaration: ComponentFileDeclaration, isPage:
676
647
  }
677
648
 
678
649
  function isPascalCaseName(name: string) {
679
- // PascalCase component names start uppercase and are not SCREAMING_SNAKE_CASE constants.
680
650
  return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
681
651
  }
682
652
 
@@ -686,31 +656,23 @@ function isDefaultExportStatement(statement: ts.Statement) {
686
656
  return !!ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
687
657
  }
688
658
 
689
- /**
690
- * Bun's bundler keeps `//!` and `/*!` through minification (it reads them as the `@license` class), so a
691
- * bang marker in browser-reachable code ships verbatim to every visitor. `no-bang-comment-in-client.grit`
692
- * is the build gate; this exists because that rule cannot see the whole file.
693
- *
694
- * A comment is trivia, which Biome's GritQL exposes to no node pattern, so the rule matches through
695
- * `file($name, $body)` — and `$body` is the module's *token* span. A marker above the first statement or
696
- * below the last one sits in leading or trailing trivia and is unreachable from GritQL at all. Reading the
697
- * raw text here covers both, and re-reporting the markers grit already catches costs nothing: the two run
698
- * from different commands, and a file with one is failing lint either way.
699
- */
659
+ // Bun keeps `//!` and `/*!` through minification. GritQL's `file($name, $body)` spans tokens only, so the grit rule
660
+ // misses a marker in the file's leading or trailing trivia; the raw text is read here.
700
661
  function getBangCommentWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
701
662
  if (!isClientReachableFile(sourceFile.file)) return [];
702
663
  // Anchored to line start or to whitespace after code, so a literal like `"https://host//!path"` is not a hit.
703
- return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => ({
704
- rule: "akan.file.bang-comment-in-client",
705
- scope: "file" as const,
706
- severity: "warning" as const,
707
- file: sourceFile.file,
708
- line,
709
- message: "A `//!` or `/*!` marker survives minification and ships to the browser.",
710
- }));
664
+ return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) =>
665
+ fileWarning(
666
+ "akan.file.bang-comment-in-client",
667
+ "file",
668
+ sourceFile.file,
669
+ "A `//!` or `/*!` marker survives minification and ships to the browser.",
670
+ line,
671
+ ),
672
+ );
711
673
  }
712
674
 
713
- /** Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`. */
675
+ // Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`.
714
676
  function isClientReachableFile(file: string) {
715
677
  if (/\.(test|spec)\.tsx?$/.test(file)) return false;
716
678
  if (/\.(constant|store)\.ts$/.test(file)) return true;
@@ -725,67 +687,53 @@ function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarnin
725
687
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx")) return [];
726
688
  return getTopLevelDeclarations(sourceFile)
727
689
  .filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name))
728
- .map((declaration) => ({
729
- rule: "akan.file.placeholder-export",
730
- scope: "file",
731
- severity: "warning",
732
- file: sourceFile.file,
733
- line: declaration.line,
734
- message: `Generated or barrel index file should not export placeholder "${declaration.name}".`,
735
- }));
690
+ .map((declaration) =>
691
+ fileWarning(
692
+ "akan.file.placeholder-export",
693
+ "file",
694
+ sourceFile.file,
695
+ `Generated or barrel index file should not export placeholder "${declaration.name}".`,
696
+ declaration.line,
697
+ ),
698
+ );
736
699
  }
737
700
 
738
701
  function getDictionaryTextWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
739
702
  if (!sourceFile.file.endsWith(".dictionary.ts")) return [];
740
703
  const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
741
704
  return stalePatterns.flatMap(({ pattern, label }) =>
742
- findPatternLines(sourceFile.content, pattern).map((line) => ({
743
- rule: "akan.file.dictionary-stale-text",
744
- scope: "file",
745
- severity: "warning",
746
- file: sourceFile.file,
747
- line,
748
- message: `Dictionary text appears to contain ${label}.`,
749
- })),
705
+ findPatternLines(sourceFile.content, pattern).map((line) =>
706
+ fileWarning(
707
+ "akan.file.dictionary-stale-text",
708
+ "file",
709
+ sourceFile.file,
710
+ `Dictionary text appears to contain ${label}.`,
711
+ line,
712
+ ),
713
+ ),
750
714
  );
751
715
  }
752
716
 
753
717
  function getGlobalMutationWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
754
718
  const warnings: QualityWarning[] = [];
755
719
  for (const statement of sourceFile.sourceFile.statements) {
756
- if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
757
- warnings.push({
758
- rule: "akan.file.global-declaration",
759
- scope: "file",
760
- severity: "warning",
761
- file: sourceFile.file,
762
- line: getLine(sourceFile.sourceFile, statement),
763
- message: "Global declarations require an explicit low-level integration allowlist.",
764
- });
765
- }
766
- if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
767
- warnings.push({
768
- rule: "akan.file.window-augmentation",
769
- scope: "file",
770
- severity: "warning",
771
- file: sourceFile.file,
772
- line: getLine(sourceFile.sourceFile, statement),
773
- message: "Window augmentation should be isolated to approved browser integration files.",
774
- });
775
- }
720
+ const warn = (rule: string, message: string) =>
721
+ warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
722
+ if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
723
+ warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
724
+ if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window")
725
+ warn(
726
+ "akan.file.window-augmentation",
727
+ "Window augmentation should be isolated to approved browser integration files.",
728
+ );
776
729
  if (
777
730
  ts.isExpressionStatement(statement) &&
778
731
  statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")
779
- ) {
780
- warnings.push({
781
- rule: "akan.file.prototype-mutation",
782
- scope: "file",
783
- severity: "warning",
784
- file: sourceFile.file,
785
- line: getLine(sourceFile.sourceFile, statement),
786
- message: "Prototype mutation should be avoided or isolated to approved low-level integration files.",
787
- });
788
- }
732
+ )
733
+ warn(
734
+ "akan.file.prototype-mutation",
735
+ "Prototype mutation should be avoided or isolated to approved low-level integration files.",
736
+ );
789
737
  }
790
738
  return warnings;
791
739
  }
@@ -798,35 +746,35 @@ function getTopLevelDeclarations(sourceFile: SourceFileInfo): TopLevelDeclaratio
798
746
 
799
747
  function getTopLevelDeclaration(sourceFile: ts.SourceFile, statement: ts.Statement): TopLevelDeclaration[] {
800
748
  const line = getLine(sourceFile, statement);
801
- if (ts.isClassDeclaration(statement) && statement.name) {
802
- return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
803
- }
804
- if (ts.isFunctionDeclaration(statement) && statement.name) {
805
- return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
806
- }
807
- if (ts.isInterfaceDeclaration(statement)) {
808
- return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
809
- }
810
- if (ts.isTypeAliasDeclaration(statement)) {
811
- return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
812
- }
813
- if (ts.isEnumDeclaration(statement)) {
814
- return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
815
- }
816
- if (ts.isVariableStatement(statement)) {
749
+ const declared = (name: string, kind: string, at = line) => ({
750
+ name,
751
+ kind,
752
+ line: at,
753
+ exported: isExported(statement),
754
+ node: statement,
755
+ });
756
+ if (ts.isClassDeclaration(statement) && statement.name) return [declared(statement.name.text, "class")];
757
+ if (ts.isFunctionDeclaration(statement) && statement.name) return [declared(statement.name.text, "function")];
758
+ if (ts.isInterfaceDeclaration(statement)) return [declared(statement.name.text, "interface")];
759
+ if (ts.isTypeAliasDeclaration(statement)) return [declared(statement.name.text, "type")];
760
+ if (ts.isEnumDeclaration(statement)) return [declared(statement.name.text, "enum")];
761
+ if (ts.isVariableStatement(statement))
817
762
  return statement.declarationList.declarations
818
763
  .filter((declaration) => ts.isIdentifier(declaration.name))
819
- .map((declaration) => ({
820
- name: (declaration.name as ts.Identifier).text,
821
- kind: "variable",
822
- line: getLine(sourceFile, declaration),
823
- exported: isExported(statement),
824
- node: statement,
825
- }));
826
- }
827
- if (ts.isExportDeclaration(statement)) {
764
+ .map((declaration) =>
765
+ declared((declaration.name as ts.Identifier).text, "variable", getLine(sourceFile, declaration)),
766
+ );
767
+ if (ts.isExportDeclaration(statement))
828
768
  return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
829
- }
769
+ return [];
770
+ }
771
+
772
+ function getConventionClassNames(suffix: (typeof CONVENTION_SUFFIXES)[number], model: string) {
773
+ if (suffix === ".constant.ts") return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
774
+ if (suffix === ".document.ts") return [`${model}Filter`, model, `${model}Model`];
775
+ if (suffix === ".service.ts") return [`${model}Service`];
776
+ if (suffix === ".signal.ts") return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
777
+ if (suffix === ".store.ts") return [`${model}Store`];
830
778
  return [];
831
779
  }
832
780
 
@@ -836,45 +784,18 @@ function isAllowedConventionDeclaration(
836
784
  declaration: TopLevelDeclaration,
837
785
  ) {
838
786
  if (suffix === ".dictionary.ts") return isExportedConst(declaration) && declaration.name === "dictionary";
839
- if (suffix === ".constant.ts") return isAllowedConstantDeclaration(modelName, declaration);
840
- if (suffix === ".document.ts")
841
- return (
842
- declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name)
843
- );
844
- if (suffix === ".service.ts") return declaration.kind === "class" && declaration.name === `${modelName}Service`;
845
- if (suffix === ".signal.ts")
846
- return (
847
- declaration.kind === "class" &&
848
- [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name)
849
- );
850
- if (suffix === ".store.ts") return declaration.kind === "class" && declaration.name === `${modelName}Store`;
851
- return false;
852
- }
853
-
854
- function isAllowedConstantDeclaration(modelName: string, declaration: TopLevelDeclaration) {
855
787
  if (declaration.kind !== "class") return false;
856
- if (
857
- [`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(
858
- declaration.name,
859
- )
860
- )
861
- return true;
862
- if (!ts.isClassDeclaration(declaration.node)) return false;
863
- const heritageClause = declaration.node.heritageClauses?.find(
864
- (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
865
- );
866
- const expression = heritageClause?.types[0]?.expression;
867
- return !!expression && expression.getText().startsWith("enumOf(");
788
+ if (getConventionClassNames(suffix, modelName).includes(declaration.name)) return true;
789
+ return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
868
790
  }
869
791
 
870
792
  function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number], modelName: string) {
871
793
  if (suffix === ".dictionary.ts") return "export const dictionary";
872
- if (suffix === ".constant.ts")
873
- return `${modelName}Input, ${modelName}Object, ${modelName}, Light${modelName}, ${modelName}Insight, or enumOf classes`;
874
- if (suffix === ".document.ts") return `${modelName}Filter, ${modelName}, or ${modelName}Model`;
875
- if (suffix === ".service.ts") return `${modelName}Service`;
876
- if (suffix === ".signal.ts") return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
877
- return `${modelName}Store`;
794
+ const names = [
795
+ ...getConventionClassNames(suffix, modelName),
796
+ ...(suffix === ".constant.ts" ? ["enumOf classes"] : []),
797
+ ];
798
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
878
799
  }
879
800
 
880
801
  const sysRootTypes = { apps: "app", libs: "lib" } as const;
@@ -910,13 +831,12 @@ function getModuleUiWarning(file: string): QualityWarning | null {
910
831
  : ["Template", "Unit"];
911
832
  const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
912
833
  if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx")) return null;
913
- return {
914
- rule: "akan.layout.module-ui-file",
915
- scope: "layout",
916
- severity: "warning",
834
+ return fileWarning(
835
+ "akan.layout.module-ui-file",
836
+ "layout",
917
837
  file,
918
- message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`,
919
- };
838
+ `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`,
839
+ );
920
840
  }
921
841
 
922
842
  function getModuleInfo(file: string) {
@@ -930,10 +850,7 @@ function getModuleInfo(file: string) {
930
850
  }
931
851
  if (segments.length !== libIndex + 3) return null;
932
852
  const fileName = segments[libIndex + 2];
933
- if (moduleName.startsWith("__")) {
934
- const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
935
- return { moduleName: scalarName, fileName, kind: "scalar" as const };
936
- }
853
+ if (moduleName.startsWith("__")) return { moduleName, fileName, kind: "scalar" as const };
937
854
  if (moduleName.startsWith("_")) return { moduleName, fileName, kind: "service" as const };
938
855
  return { moduleName, fileName, kind: "database" as const };
939
856
  }
@@ -947,10 +864,7 @@ function isExportedConst(declaration: TopLevelDeclaration) {
947
864
  }
948
865
 
949
866
  function isExported(node: ts.Node) {
950
- return (
951
- !!ts.getCombinedModifierFlags(node as ts.Declaration) &&
952
- !!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export)
953
- );
867
+ return !!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export);
954
868
  }
955
869
 
956
870
  function isFunctionLikeInitializer(node: ts.Expression | undefined) {