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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
package/qualityScanner.ts CHANGED
@@ -6,7 +6,7 @@ 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";
9
+ import { fileWarning, formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
10
10
  import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
11
11
 
12
12
  type QualitySeverity = "warning";
@@ -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,27 @@ 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}". Keep ${type} code in conventional ${type} folders.`,
429
+ ),
430
+ );
440
431
  }
441
432
 
442
433
  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
- }
434
+ if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
435
+ warnings.push(
436
+ fileWarning(
437
+ "akan.layout.lib-facet-file",
438
+ "layout",
439
+ sourceFile.file,
440
+ `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
441
+ ),
442
+ );
452
443
 
453
444
  const moduleUiWarning = getModuleUiWarning(sourceFile.file);
454
445
  if (moduleUiWarning) warnings.push(moduleUiWarning);
@@ -517,79 +508,59 @@ function formatQualityLocation(file: string | undefined, line: number | undefine
517
508
 
518
509
  function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionLike[] {
519
510
  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.
511
+ // UI component names (Card, Button) repeat across apps and libs by design; only the name check is relaxed.
522
512
  const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
513
+ const add = (
514
+ name: string,
515
+ kind: ExportedFunctionLike["kind"],
516
+ node: ts.Node,
517
+ body: ts.Node | undefined,
518
+ isEnumClass = false,
519
+ ) =>
520
+ declarations.push({
521
+ name,
522
+ kind,
523
+ file: sourceFile.file,
524
+ line: getLine(sourceFile.sourceFile, node),
525
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
526
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass),
527
+ });
523
528
  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
- }
529
+ if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement))
530
+ add(statement.name.text, "function", statement, statement.body);
531
+ if (ts.isClassDeclaration(statement) && statement.name && isExported(statement))
532
+ add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
546
533
  if (ts.isVariableStatement(statement) && isExported(statement)) {
547
534
  for (const declaration of statement.declarationList.declarations) {
548
535
  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
- });
536
+ add(declaration.name.text, "function-variable", declaration, declaration.initializer);
557
537
  }
558
538
  }
559
539
  }
560
540
  return declarations;
561
541
  }
562
542
 
563
- function isPageRouteFile(file: string) {
543
+ function sysSegments(file: string) {
564
544
  const segments = file.split("/");
565
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
545
+ return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
546
+ }
547
+
548
+ function isPageRouteFile(file: string) {
549
+ return sysSegments(file)?.[2] === "page";
566
550
  }
567
551
 
568
552
  function isUiComponentFile(file: string) {
569
- const segments = file.split("/");
570
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
553
+ return sysSegments(file)?.[2] === "ui";
571
554
  }
572
555
 
573
556
  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;
557
+ if (!sysSegments(file)?.includes("lib")) return false;
558
+ if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file)) return true;
583
559
  // Model view classes may repeat across modules; enum classes must stay uniquely named.
584
560
  if (file.endsWith(".constant.ts")) return !isEnumClass;
585
561
  return false;
586
562
  }
587
563
 
588
- function isInLibModule(file: string) {
589
- const segments = file.split("/");
590
- return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
591
- }
592
-
593
564
  function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement) {
594
565
  if (!ts.isClassDeclaration(statement)) return false;
595
566
  const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
@@ -646,9 +617,7 @@ function getComponentFileDeclarations(sourceFile: ts.SourceFile): ComponentFileD
646
617
  return declarations;
647
618
  }
648
619
 
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.
620
+ // `Like.WithDislike = WithDislike` makes both names components, so the local definition and its Props may stay unexported.
652
621
  function getCompoundComponentNames(sourceFile: ts.SourceFile): Set<string> {
653
622
  const names = new Set<string>();
654
623
  for (const statement of sourceFile.statements) {
@@ -676,7 +645,6 @@ function isAllowedComponentExport(declaration: ComponentFileDeclaration, isPage:
676
645
  }
677
646
 
678
647
  function isPascalCaseName(name: string) {
679
- // PascalCase component names start uppercase and are not SCREAMING_SNAKE_CASE constants.
680
648
  return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
681
649
  }
682
650
 
@@ -686,31 +654,23 @@ function isDefaultExportStatement(statement: ts.Statement) {
686
654
  return !!ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
687
655
  }
688
656
 
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
- */
657
+ // Bun keeps `//!` and `/*!` through minification. GritQL's `file($name, $body)` spans tokens only, so the grit rule
658
+ // misses a marker in the file's leading or trailing trivia; the raw text is read here.
700
659
  function getBangCommentWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
701
660
  if (!isClientReachableFile(sourceFile.file)) return [];
702
661
  // 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
- }));
662
+ return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) =>
663
+ fileWarning(
664
+ "akan.file.bang-comment-in-client",
665
+ "file",
666
+ sourceFile.file,
667
+ "A `//!` or `/*!` marker survives minification and ships to the browser.",
668
+ line,
669
+ ),
670
+ );
711
671
  }
712
672
 
713
- /** Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`. */
673
+ // Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`.
714
674
  function isClientReachableFile(file: string) {
715
675
  if (/\.(test|spec)\.tsx?$/.test(file)) return false;
716
676
  if (/\.(constant|store)\.ts$/.test(file)) return true;
@@ -725,67 +685,53 @@ function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarnin
725
685
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx")) return [];
726
686
  return getTopLevelDeclarations(sourceFile)
727
687
  .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
- }));
688
+ .map((declaration) =>
689
+ fileWarning(
690
+ "akan.file.placeholder-export",
691
+ "file",
692
+ sourceFile.file,
693
+ `Generated or barrel index file should not export placeholder "${declaration.name}".`,
694
+ declaration.line,
695
+ ),
696
+ );
736
697
  }
737
698
 
738
699
  function getDictionaryTextWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
739
700
  if (!sourceFile.file.endsWith(".dictionary.ts")) return [];
740
701
  const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
741
702
  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
- })),
703
+ findPatternLines(sourceFile.content, pattern).map((line) =>
704
+ fileWarning(
705
+ "akan.file.dictionary-stale-text",
706
+ "file",
707
+ sourceFile.file,
708
+ `Dictionary text appears to contain ${label}.`,
709
+ line,
710
+ ),
711
+ ),
750
712
  );
751
713
  }
752
714
 
753
715
  function getGlobalMutationWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
754
716
  const warnings: QualityWarning[] = [];
755
717
  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
- }
718
+ const warn = (rule: string, message: string) =>
719
+ warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
720
+ if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
721
+ warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
722
+ if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window")
723
+ warn(
724
+ "akan.file.window-augmentation",
725
+ "Window augmentation should be isolated to approved browser integration files.",
726
+ );
776
727
  if (
777
728
  ts.isExpressionStatement(statement) &&
778
729
  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
- }
730
+ )
731
+ warn(
732
+ "akan.file.prototype-mutation",
733
+ "Prototype mutation should be avoided or isolated to approved low-level integration files.",
734
+ );
789
735
  }
790
736
  return warnings;
791
737
  }
@@ -798,35 +744,35 @@ function getTopLevelDeclarations(sourceFile: SourceFileInfo): TopLevelDeclaratio
798
744
 
799
745
  function getTopLevelDeclaration(sourceFile: ts.SourceFile, statement: ts.Statement): TopLevelDeclaration[] {
800
746
  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)) {
747
+ const declared = (name: string, kind: string, at = line) => ({
748
+ name,
749
+ kind,
750
+ line: at,
751
+ exported: isExported(statement),
752
+ node: statement,
753
+ });
754
+ if (ts.isClassDeclaration(statement) && statement.name) return [declared(statement.name.text, "class")];
755
+ if (ts.isFunctionDeclaration(statement) && statement.name) return [declared(statement.name.text, "function")];
756
+ if (ts.isInterfaceDeclaration(statement)) return [declared(statement.name.text, "interface")];
757
+ if (ts.isTypeAliasDeclaration(statement)) return [declared(statement.name.text, "type")];
758
+ if (ts.isEnumDeclaration(statement)) return [declared(statement.name.text, "enum")];
759
+ if (ts.isVariableStatement(statement))
817
760
  return statement.declarationList.declarations
818
761
  .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)) {
762
+ .map((declaration) =>
763
+ declared((declaration.name as ts.Identifier).text, "variable", getLine(sourceFile, declaration)),
764
+ );
765
+ if (ts.isExportDeclaration(statement))
828
766
  return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
829
- }
767
+ return [];
768
+ }
769
+
770
+ function getConventionClassNames(suffix: (typeof CONVENTION_SUFFIXES)[number], model: string) {
771
+ if (suffix === ".constant.ts") return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
772
+ if (suffix === ".document.ts") return [`${model}Filter`, model, `${model}Model`];
773
+ if (suffix === ".service.ts") return [`${model}Service`];
774
+ if (suffix === ".signal.ts") return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
775
+ if (suffix === ".store.ts") return [`${model}Store`];
830
776
  return [];
831
777
  }
832
778
 
@@ -836,45 +782,18 @@ function isAllowedConventionDeclaration(
836
782
  declaration: TopLevelDeclaration,
837
783
  ) {
838
784
  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
785
  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(");
786
+ if (getConventionClassNames(suffix, modelName).includes(declaration.name)) return true;
787
+ return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
868
788
  }
869
789
 
870
790
  function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number], modelName: string) {
871
791
  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`;
792
+ const names = [
793
+ ...getConventionClassNames(suffix, modelName),
794
+ ...(suffix === ".constant.ts" ? ["enumOf classes"] : []),
795
+ ];
796
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
878
797
  }
879
798
 
880
799
  const sysRootTypes = { apps: "app", libs: "lib" } as const;
@@ -910,13 +829,12 @@ function getModuleUiWarning(file: string): QualityWarning | null {
910
829
  : ["Template", "Unit"];
911
830
  const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
912
831
  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",
832
+ return fileWarning(
833
+ "akan.layout.module-ui-file",
834
+ "layout",
917
835
  file,
918
- message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`,
919
- };
836
+ `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`,
837
+ );
920
838
  }
921
839
 
922
840
  function getModuleInfo(file: string) {
@@ -930,10 +848,7 @@ function getModuleInfo(file: string) {
930
848
  }
931
849
  if (segments.length !== libIndex + 3) return null;
932
850
  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
- }
851
+ if (moduleName.startsWith("__")) return { moduleName, fileName, kind: "scalar" as const };
937
852
  if (moduleName.startsWith("_")) return { moduleName, fileName, kind: "service" as const };
938
853
  return { moduleName, fileName, kind: "database" as const };
939
854
  }
@@ -947,10 +862,7 @@ function isExportedConst(declaration: TopLevelDeclaration) {
947
862
  }
948
863
 
949
864
  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
- );
865
+ return !!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export);
954
866
  }
955
867
 
956
868
  function isFunctionLikeInitializer(node: ts.Expression | undefined) {