@akanjs/cli 3.0.0-beta.9 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-zfab7pkn.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-0zhnr0pw.js +1457 -0
  6. package/README.ko.md +83 -28
  7. package/README.md +70 -27
  8. package/TunnelShare-cwh9ezsm.js +9 -0
  9. package/{agent.command-0q17pnrs.js → agent.command-aja6z9mn.js} +8 -9
  10. package/akanCode.js +102 -0
  11. package/application.command-049ehgx3.js +246 -0
  12. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-75n5rpwp.js} +121 -77
  13. package/buildBatch.proc.js +120 -32
  14. package/cloud.command-gw4kqxt8.js +95 -0
  15. package/cloud.script-c723cpcb.js +20 -0
  16. package/code.command-x9t6z5nw.js +154 -0
  17. package/commandManifest.json +1 -1
  18. package/consoleToStderr-h5bryj94.js +7 -0
  19. package/context.command-yfbe1rtd.js +48 -0
  20. package/context.runner-erfe68j5.js +21 -0
  21. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  22. package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
  23. package/fileSys-wzzrntkw.js +9 -0
  24. package/guideline.command-kq9gp690.js +42 -0
  25. package/guidelines/agentRule/agentRule.instruction.md +154 -26
  26. package/guidelines/conventions/conventions.instruction.md +138 -48
  27. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  28. package/guidelines/queryRule/queryRule.instruction.md +21 -7
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +10 -8
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
  33. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  34. package/incrementalBuilder.proc.js +564 -109
  35. package/{index-2tv3fdx4.js → index-150s07n8.js} +1230 -1459
  36. package/{index-vyvre9en.js → index-25fvrxmy.js} +6 -2
  37. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  38. package/index-2wc41k5w.js +49 -0
  39. package/index-3hx1z96c.js +87 -0
  40. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  41. package/{index-04bk599g.js → index-5g4rkrvt.js} +10 -17
  42. package/index-5pvxf7z0.js +53 -0
  43. package/index-5q14pwzd.js +69 -0
  44. package/{index-vxtad5bd.js → index-66yspb67.js} +5 -8
  45. package/index-7md2g9vw.js +6 -0
  46. package/index-7p4c0yer.js +1058 -0
  47. package/{index-d43r08tf.js → index-8b639zes.js} +77 -150
  48. package/{index-ktb5njgy.js → index-8kvy8qb4.js} +49 -99
  49. package/index-94s080qm.js +266 -0
  50. package/index-9dp031by.js +12 -0
  51. package/{index-0g7r8xz3.js → index-a37by9hn.js} +114 -232
  52. package/{index-k2j1hc4r.js → index-b95yhqjr.js} +12 -12
  53. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  54. package/{index-2xp0kfkt.js → index-br8pkrk1.js} +37 -25
  55. package/index-c14nsvnv.js +700 -0
  56. package/index-c6eggbee.js +123 -0
  57. package/index-d2242ksy.js +99 -0
  58. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  59. package/{index-a64esmtb.js → index-fkvb79qe.js} +523 -1022
  60. package/index-h54yj9wr.js +1119 -0
  61. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  62. package/{index-y6n8nmny.js → index-kn5d4jmg.js} +75 -203
  63. package/index-m3xsb1ms.js +4300 -0
  64. package/{index-gyd220p2.js → index-mb59b8x0.js} +554 -577
  65. package/{index-j9yhg404.js → index-mzjb6zp4.js} +4 -7
  66. package/index-p6pbz9n5.js +25 -0
  67. package/{index-sj5vxr8f.js → index-qe71g4ze.js} +983 -898
  68. package/index-rfcz04w9.js +187 -0
  69. package/{index-hwkygn9s.js → index-senscydg.js} +331 -36
  70. package/{index-ffvk10zz.js → index-vs8mefhy.js} +107 -241
  71. package/index-wa5jbxj1.js +760 -0
  72. package/index-y5ga5w9t.js +2729 -0
  73. package/{index-9077wzd2.js → index-y7hx1ygz.js} +43 -26
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-qzzbj6d8.js → library.command-keqjevpp.js} +4 -5
  77. package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-pydeq5zf.js} +23 -43
  78. package/{module.command-rq5hq3ax.js → module.command-dkrnqx3v.js} +8 -12
  79. package/{package.command-d9hd6v9n.js → package.command-wmc0ys5y.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-126d56te.js → page.command-tf3erhrv.js} +4 -5
  82. package/{primitive.command-taxd1538.js → primitive.command-7hqpegj4.js} +9 -23
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-6gsna0a3.js → quality.command-8122axq3.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-tq2dgy72.js → repair.command-mcy12h0n.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-3bcmrrkv.js → scalar.command-vvvbzttc.js} +7 -11
  89. package/skills/akan-code/SKILL.md +130 -0
  90. package/skills/akan-debug/SKILL.md +42 -0
  91. package/skills/akan-deps/SKILL.md +50 -0
  92. package/skills/akan-err/SKILL.md +39 -0
  93. package/skills/akan-module/SKILL.md +45 -0
  94. package/skills/akan-scaffold/SKILL.md +76 -0
  95. package/skills/akan-scalar/SKILL.md +52 -0
  96. package/skills/akan-service/SKILL.md +34 -0
  97. package/skills/akan-store/SKILL.md +67 -0
  98. package/skills/akan-ui/SKILL.md +63 -0
  99. package/skills/akan-validate/SKILL.md +44 -0
  100. package/{subspace.command-eddzqpx3.js → subspace.command-pzvqfy5h.js} +53 -62
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/app/page/_index.tsx +257 -258
  103. package/templates/app/page/_layout.tsx +5 -1
  104. package/templates/app/page/_overrides.tsx +12 -0
  105. package/templates/app/page/styles.css.template +316 -14
  106. package/templates/app/public/apple-touch-icon.png +0 -0
  107. package/templates/app/public/favicon.ico +0 -0
  108. package/templates/app/public/jelly/cloud.webp +0 -0
  109. package/templates/app/public/jelly/comet.webp +0 -0
  110. package/templates/app/public/jelly/moon.webp +0 -0
  111. package/templates/app/public/jelly/planet.webp +0 -0
  112. package/templates/app/public/jelly/rocket.webp +0 -0
  113. package/templates/app/public/jelly/star.webp +0 -0
  114. package/templates/app/public/jelly/sugar.webp +0 -0
  115. package/templates/app/public/logo.png +0 -0
  116. package/templates/app/ui/Recipe/index.ts +9 -0
  117. package/templates/app/ui/Recipe/jellyButton.ts +63 -0
  118. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  119. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  120. package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
  121. package/templates/appSample/lib/task/Task.Util.tsx +10 -13
  122. package/templates/appSample/lib/task/Task.View.tsx +2 -2
  123. package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
  124. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  125. package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
  126. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  127. package/templates/appSample/lib/task/task.signal.ts +10 -8
  128. package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
  129. package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
  130. package/templates/appSample/page/task/_index.tsx +4 -3
  131. package/templates/appSample/page/task/_layout.tsx +1 -1
  132. package/templates/appSample/page/task/new.tsx +5 -4
  133. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  134. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  135. package/templates/appSample/ui/GlobalLoading.tsx +2 -1
  136. package/templates/appSample/ui/QuantityControl.tsx +3 -3
  137. package/templates/client.ts +7 -30
  138. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  139. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  140. package/templates/crudPages/new/page.tsx +2 -1
  141. package/templates/crudPages/page.tsx +2 -1
  142. package/templates/crudSinglePage/page.tsx +2 -1
  143. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  144. package/templates/lib/__lib/lib.document.ts +0 -2
  145. package/templates/lib/sig.ts +8 -30
  146. package/templates/localDev/docker-compose.yaml.template +3 -14
  147. package/templates/module/__model__.signal.ts +12 -4
  148. package/templates/pkgRoot/tsconfig.json.template +2 -7
  149. package/templates/server.ts +1 -3
  150. package/templates/service/__model__.abstract.md.template +8 -0
  151. package/templates/workspaceRoot/.gitignore.template +0 -1
  152. package/templates/workspaceRoot/AGENTS.md.template +6 -0
  153. package/templates/workspaceRoot/package.json.template +1 -3
  154. package/tunnel.command-82zz5xnq.js +129 -0
  155. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  156. package/typecheck.proc.js +1 -1
  157. package/workflow.command-y8x3kgjv.js +79 -0
  158. package/{workspace.command-06wvstgr.js → workspace.command-jvsb7brc.js} +79 -109
  159. package/abstractCompactor-gmkc0z8j.js +0 -102
  160. package/application.command-18y0s485.js +0 -204
  161. package/applicationReleasePackager-4yasxwv9.js +0 -244
  162. package/capacitorApp-36rbr858.js +0 -12
  163. package/cloud.command-av97ndh6.js +0 -94
  164. package/cloud.script-cm11xjdz.js +0 -20
  165. package/context.command-5v37ef3n.js +0 -81
  166. package/devTuiView-0hc2emhe.js +0 -9
  167. package/getDirname-dr3jz1xg.js +0 -7
  168. package/guideline.command-4pz9g1rt.js +0 -360
  169. package/guidelines/agentRule/agentRule.generate.json +0 -31
  170. package/guidelines/componentRule/componentRule.generate.json +0 -31
  171. package/guidelines/conventions/conventions.generate.json +0 -25
  172. package/guidelines/cssRule/cssRule.generate.json +0 -31
  173. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  174. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  175. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  176. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  177. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  178. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  179. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  180. package/guidelines/framework/framework.generate.json +0 -32
  181. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  182. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  183. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  184. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  185. package/guidelines/modelService/modelService.generate.json +0 -31
  186. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  187. package/guidelines/modelStore/modelStore.generate.json +0 -31
  188. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  189. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  190. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  191. package/guidelines/modelView/modelView.generate.json +0 -31
  192. package/guidelines/modelZone/modelZone.generate.json +0 -31
  193. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  194. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  195. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  196. package/guidelines/queryRule/queryRule.generate.json +0 -31
  197. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  198. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  199. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  200. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  201. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  202. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  203. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  204. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  205. package/guidelines/transportRule/transportRule.generate.json +0 -31
  206. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  207. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  208. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  209. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  210. package/index-47pj3aee.js +0 -76
  211. package/index-5ystnsd5.js +0 -841
  212. package/index-6wwzs1bc.js +0 -7
  213. package/index-a17x49vg.js +0 -229
  214. package/index-m0xccmmm.js +0 -228
  215. package/index-meh558jy.js +0 -65
  216. package/index-rtxxan88.js +0 -471
  217. package/index-te54x9zm.js +0 -338
  218. package/index-w8kmx1b6.js +0 -2176
  219. package/index-wt7qrtd4.js +0 -1516
  220. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  221. package/templates/module/__model__.abstract.md +0 -28
  222. package/templates/service/__model__.abstract.md +0 -28
  223. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  224. package/workflow.command-gtdnzvbz.js +0 -108
@@ -1,29 +1,89 @@
1
1
  // @bun
2
2
  import {
3
3
  Workspace,
4
- runner,
5
4
  script,
6
5
  command
7
- } from "./index-0g7r8xz3.js";
8
- import"./index-q0y2jq9j.js";
6
+ } from "./index-a37by9hn.js";
7
+ import"./index-52w0fvh8.js";
9
8
  import {
10
9
  rootAllowedFiles,
11
10
  rootAllowedDirs,
12
- isAllowedLibFacetRootFile
13
- } from "./index-2tv3fdx4.js";
14
- import"./index-6wwzs1bc.js";
15
- import {
16
- AbstractDoc
17
- } from "./index-meh558jy.js";
11
+ isAllowedLibFacetRootFile,
12
+ rootEntryHintOf
13
+ } from "./index-150s07n8.js";
18
14
 
19
15
  // pkgs/@akanjs/devkit/qualityScanner.ts
20
16
  import { createHash } from "crypto";
21
17
  import { readdir, readFile, stat } from "fs/promises";
22
- import path from "path";
18
+ import path2 from "path";
23
19
  import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
24
20
  import ignore from "ignore";
25
21
  import ts3 from "typescript";
26
22
 
23
+ // pkgs/@akanjs/devkit/abstractDoc.ts
24
+ import path from "path";
25
+
26
+ class AbstractDoc {
27
+ path;
28
+ content;
29
+ siblingFiles;
30
+ static suffix = ".abstract.md";
31
+ static maxLines = 300;
32
+ static #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
33
+ static isAbstractPath(filePath) {
34
+ return filePath.endsWith(AbstractDoc.suffix);
35
+ }
36
+ static lineCountOf(content) {
37
+ return content.split(/\r?\n/).length;
38
+ }
39
+ static kindOf(filePath) {
40
+ const [root, folder, ...rest] = filePath.split("/");
41
+ if (root !== "lib" || !folder)
42
+ return "other";
43
+ if (folder === "__scalar")
44
+ return rest.length === 2 ? "scalar" : "other";
45
+ if (rest.length !== 1)
46
+ return "other";
47
+ return folder.startsWith("_") ? "service" : "domain";
48
+ }
49
+ static async findAll(sys, { module } = {}) {
50
+ const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`)).map((filePath) => filePath.split(path.sep).join("/")).sort();
51
+ const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
52
+ if (!module)
53
+ return docs;
54
+ return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
55
+ }
56
+ static async read(sys, filePath) {
57
+ const dirPath = filePath.split("/").slice(0, -1).join("/");
58
+ const [content, folderFiles] = await Promise.all([sys.readFile(filePath), sys.readdir(dirPath)]);
59
+ return new AbstractDoc(filePath, content, folderFiles.filter((fileName) => !AbstractDoc.isAbstractPath(fileName)));
60
+ }
61
+ constructor(path2, content, siblingFiles) {
62
+ this.path = path2;
63
+ this.content = content;
64
+ this.siblingFiles = siblingFiles;
65
+ }
66
+ get kind() {
67
+ return AbstractDoc.kindOf(this.path);
68
+ }
69
+ get folderName() {
70
+ return this.path.split("/").at(-2) ?? "";
71
+ }
72
+ get moduleName() {
73
+ return (this.path.split("/").at(-1) ?? "").slice(0, -AbstractDoc.suffix.length);
74
+ }
75
+ get lineCount() {
76
+ return AbstractDoc.lineCountOf(this.content);
77
+ }
78
+ canReplaceWith(candidate) {
79
+ const next = candidate.trim();
80
+ if (!next.startsWith("#"))
81
+ return false;
82
+ const lineCount = AbstractDoc.lineCountOf(next);
83
+ return lineCount >= 3 && lineCount < this.lineCount;
84
+ }
85
+ }
86
+
27
87
  // pkgs/@akanjs/devkit/formSetterScanner.ts
28
88
  import ts from "typescript";
29
89
 
@@ -99,25 +159,11 @@ var STATIC_COMPONENT_MIN_MASS = 4;
99
159
  var MIXED_COMPONENT_MIN_MASS = 10;
100
160
  var MIXED_COMPONENT_MAX_TOUCHES = 2;
101
161
  var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
162
+ var fileWarning = (rule, scope, file, message, line) => line === undefined ? { rule, scope, severity: "warning", file, message } : { rule, scope, severity: "warning", file, line, message };
102
163
 
103
164
  class SsrScanner {
104
165
  static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
105
- static #clientGlobals = new Set([
106
- "window",
107
- "document",
108
- "navigator",
109
- "localStorage",
110
- "sessionStorage",
111
- "location",
112
- "history",
113
- "screen",
114
- "matchMedia",
115
- "IntersectionObserver",
116
- "ResizeObserver",
117
- "MutationObserver",
118
- "requestAnimationFrame",
119
- "WebSocket"
120
- ]);
166
+ static #clientGlobals = new Set("window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver".split(" ").concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")));
121
167
  static #clientRuntimeImports = new Set(["st", "fetch"]);
122
168
  scan(sourceFiles) {
123
169
  const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
@@ -138,38 +184,17 @@ class SsrScanner {
138
184
  const components = this.#getComponents(sourceFile, vendorNames);
139
185
  const warnings = [];
140
186
  if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
141
- warnings.push({
142
- rule: "akan.ssr.unnecessary-use-client",
143
- scope: "ssr",
144
- severity: "warning",
145
- file: sourceFile.file,
146
- line: 1,
147
- message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
148
- });
187
+ warnings.push(fileWarning("akan.ssr.unnecessary-use-client", "ssr", sourceFile.file, `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`, 1));
149
188
  }
150
189
  for (const component of components) {
151
190
  if (component.vendorTags)
152
191
  continue;
153
192
  if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
154
- warnings.push({
155
- rule: "akan.ssr.client-static-component",
156
- scope: "ssr",
157
- severity: "warning",
158
- file: sourceFile.file,
159
- line: component.line,
160
- message: `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`
161
- });
193
+ warnings.push(fileWarning("akan.ssr.client-static-component", "ssr", sourceFile.file, `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`, component.line));
162
194
  continue;
163
195
  }
164
196
  if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
165
- warnings.push({
166
- rule: "akan.ssr.client-static-markup",
167
- scope: "ssr",
168
- severity: "warning",
169
- file: sourceFile.file,
170
- line: component.line,
171
- message: `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`
172
- });
197
+ warnings.push(fileWarning("akan.ssr.client-static-markup", "ssr", sourceFile.file, `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`, component.line));
173
198
  }
174
199
  }
175
200
  warnings.push(...this.#getMountLoadWarnings(sourceFile));
@@ -189,32 +214,16 @@ class SsrScanner {
189
214
  entry.serverFiles += 1;
190
215
  modules.set(moduleDir, entry);
191
216
  }
192
- return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
193
- rule: "akan.ssr.module-missing-server-view",
194
- scope: "ssr",
195
- severity: "warning",
196
- file: entry.line,
197
- message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
198
- }));
217
+ return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => fileWarning("akan.ssr.module-missing-server-view", "ssr", entry.line, `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`));
199
218
  }
200
219
  #getMountLoadWarnings(sourceFile) {
201
220
  const warnings = [];
202
- const visit = (node) => {
203
- if (this.#isMountEffect(sourceFile.sourceFile, node)) {
204
- for (const load of this.#getLoadCalls(sourceFile.sourceFile, node)) {
205
- warnings.push({
206
- rule: "akan.ssr.client-mount-load",
207
- scope: "ssr",
208
- severity: "warning",
209
- file: sourceFile.file,
210
- line: this.#getLine(sourceFile.sourceFile, load.node),
211
- message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
212
- });
213
- }
214
- }
215
- ts2.forEachChild(node, visit);
216
- };
217
- ts2.forEachChild(sourceFile.sourceFile, visit);
221
+ SsrScanner.#walk(sourceFile.sourceFile, (node) => {
222
+ if (!this.#isMountEffect(sourceFile.sourceFile, node))
223
+ return;
224
+ for (const load of this.#getLoadCalls(sourceFile.sourceFile, node))
225
+ warnings.push(fileWarning("akan.ssr.client-mount-load", "ssr", sourceFile.file, `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`, this.#getLine(sourceFile.sourceFile, load.node)));
226
+ });
218
227
  return warnings;
219
228
  }
220
229
  #isMountEffect(sourceFile, node) {
@@ -228,35 +237,23 @@ class SsrScanner {
228
237
  }
229
238
  #getLoadCalls(sourceFile, node) {
230
239
  const calls = [];
231
- const visit = (child) => {
232
- if (ts2.isCallExpression(child)) {
233
- const callee = child.expression.getText(sourceFile);
234
- if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
235
- calls.push({ callee, node: child });
236
- }
237
- ts2.forEachChild(child, visit);
238
- };
239
- ts2.forEachChild(node, visit);
240
+ SsrScanner.#walk(node, (child) => {
241
+ if (!ts2.isCallExpression(child))
242
+ return;
243
+ const callee = child.expression.getText(sourceFile);
244
+ if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
245
+ calls.push({ callee, node: child });
246
+ });
240
247
  return calls;
241
248
  }
242
249
  #getTemplateStateWarnings(sourceFile) {
243
250
  if (!sourceFile.file.endsWith(".Template.tsx"))
244
251
  return [];
245
252
  const warnings = [];
246
- const visit = (node) => {
247
- if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
248
- warnings.push({
249
- rule: "akan.ssr.template-client-state",
250
- scope: "ssr",
251
- severity: "warning",
252
- file: sourceFile.file,
253
- line: this.#getLine(sourceFile.sourceFile, node),
254
- message: "Template holds form state in useState. Templates are store-driven and carry no local state."
255
- });
256
- }
257
- ts2.forEachChild(node, visit);
258
- };
259
- ts2.forEachChild(sourceFile.sourceFile, visit);
253
+ SsrScanner.#walk(sourceFile.sourceFile, (node) => {
254
+ if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
255
+ warnings.push(fileWarning("akan.ssr.template-client-state", "ssr", sourceFile.file, "Template holds form state in useState. Templates are store-driven and carry no local state.", this.#getLine(sourceFile.sourceFile, node)));
256
+ });
260
257
  return warnings;
261
258
  }
262
259
  #measureBalance(sourceFiles) {
@@ -311,7 +308,7 @@ class SsrScanner {
311
308
  }
312
309
  #getTouches(sourceFile, node) {
313
310
  const touches = [];
314
- const visit = (child) => {
311
+ SsrScanner.#walk(node, (child) => {
315
312
  if (ts2.isCallExpression(child)) {
316
313
  const callee = child.expression.getText(sourceFile);
317
314
  const bareName = callee.split(".").pop() ?? callee;
@@ -329,31 +326,32 @@ class SsrScanner {
329
326
  else if (SsrScanner.#clientGlobals.has(root))
330
327
  touches.push(root);
331
328
  }
332
- ts2.forEachChild(child, visit);
333
- };
334
- ts2.forEachChild(node, visit);
329
+ });
335
330
  return touches;
336
331
  }
337
332
  #getTagNames(sourceFile, node) {
338
333
  const tags = new Set;
339
- const visit = (child) => {
334
+ SsrScanner.#walk(node, (child) => {
340
335
  if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
341
336
  tags.add(child.tagName.getText(sourceFile).split(".")[0]);
342
- ts2.forEachChild(child, visit);
343
- };
344
- ts2.forEachChild(node, visit);
337
+ });
345
338
  return tags;
346
339
  }
347
340
  #getMass(node) {
348
341
  let mass = 0;
349
- const visit = (child) => {
342
+ SsrScanner.#walk(node, (child) => {
350
343
  if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
351
344
  mass += 1;
352
- ts2.forEachChild(child, visit);
353
- };
354
- ts2.forEachChild(node, visit);
345
+ });
355
346
  return mass;
356
347
  }
348
+ static #walk(node, visit) {
349
+ const walk = (child) => {
350
+ visit(child);
351
+ ts2.forEachChild(child, walk);
352
+ };
353
+ ts2.forEachChild(node, walk);
354
+ }
357
355
  #hasUseClient(sourceFile) {
358
356
  const first = sourceFile.statements[0];
359
357
  if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
@@ -491,7 +489,7 @@ var RULE_FIXES = {
491
489
  "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
492
490
  "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
493
491
  "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
494
- "akan.file.abstract-max-lines": "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.",
492
+ "akan.file.abstract-max-lines": "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.",
495
493
  "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
496
494
  "akan.file.bang-comment-in-client": "Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
497
495
  "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
@@ -549,7 +547,7 @@ class AkanQualityScanner {
549
547
  const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
550
548
  const files = [];
551
549
  for (const targetRoot of ["apps", "libs"]) {
552
- const absoluteTargetRoot = path.join(workspaceRoot, targetRoot);
550
+ const absoluteTargetRoot = path2.join(workspaceRoot, targetRoot);
553
551
  if (!await isDirectory(absoluteTargetRoot))
554
552
  continue;
555
553
  await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
@@ -557,7 +555,7 @@ class AkanQualityScanner {
557
555
  return files.sort();
558
556
  }
559
557
  async#readGitIgnore(workspaceRoot) {
560
- const gitIgnorePath = path.join(workspaceRoot, ".gitignore");
558
+ const gitIgnorePath = path2.join(workspaceRoot, ".gitignore");
561
559
  if (!await Bun.file(gitIgnorePath).exists())
562
560
  return [];
563
561
  return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
@@ -565,8 +563,8 @@ class AkanQualityScanner {
565
563
  async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
566
564
  const entries = await readdir(currentPath, { withFileTypes: true });
567
565
  for (const entry of entries) {
568
- const absolutePath = path.join(currentPath, entry.name);
569
- const relativePath = toPosix(path.relative(workspaceRoot, absolutePath));
566
+ const absolutePath = path2.join(currentPath, entry.name);
567
+ const relativePath = toPosix(path2.relative(workspaceRoot, absolutePath));
570
568
  if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
571
569
  continue;
572
570
  if (entry.isDirectory()) {
@@ -581,7 +579,7 @@ class AkanQualityScanner {
581
579
  }
582
580
  }
583
581
  async#readSourceFile(workspaceRoot, file) {
584
- const absolutePath = path.join(workspaceRoot, file);
582
+ const absolutePath = path2.join(workspaceRoot, file);
585
583
  const content = await readFile(absolutePath, "utf8");
586
584
  return {
587
585
  file,
@@ -591,7 +589,7 @@ class AkanQualityScanner {
591
589
  };
592
590
  }
593
591
  async#readTextFile(workspaceRoot, file) {
594
- return { file, content: await readFile(path.join(workspaceRoot, file), "utf8") };
592
+ return { file, content: await readFile(path2.join(workspaceRoot, file), "utf8") };
595
593
  }
596
594
  #scanGlobalQuality(sourceFiles) {
597
595
  const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
@@ -627,24 +625,10 @@ class AkanQualityScanner {
627
625
  const warnings = [];
628
626
  const lineCount = sourceFile.content.split(/\r?\n/).length;
629
627
  const recommendedLineLimit = getRecommendedLineLimit(sourceFile.file);
630
- if (recommendedLineLimit && lineCount > recommendedLineLimit) {
631
- warnings.push({
632
- rule: "akan.file.recommended-max-lines",
633
- scope: "file",
634
- severity: "warning",
635
- file: sourceFile.file,
636
- message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
637
- });
638
- }
639
- if (lineCount > MAX_FILE_LINES) {
640
- warnings.push({
641
- rule: "akan.file.max-lines",
642
- scope: "file",
643
- severity: "warning",
644
- file: sourceFile.file,
645
- message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
646
- });
647
- }
628
+ if (recommendedLineLimit && lineCount > recommendedLineLimit)
629
+ warnings.push(fileWarning("akan.file.recommended-max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`));
630
+ if (lineCount > MAX_FILE_LINES)
631
+ warnings.push(fileWarning("akan.file.max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`));
648
632
  warnings.push(...getBangCommentWarnings(sourceFile));
649
633
  warnings.push(...getPlaceholderExportWarnings(sourceFile));
650
634
  warnings.push(...getDictionaryTextWarnings(sourceFile));
@@ -658,14 +642,7 @@ class AkanQualityScanner {
658
642
  continue;
659
643
  if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
660
644
  continue;
661
- warnings.push({
662
- rule: "akan.file.class-export-global-declaration",
663
- scope: "file",
664
- severity: "warning",
665
- file: sourceFile.file,
666
- line: declaration.line,
667
- message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
668
- });
645
+ warnings.push(fileWarning("akan.file.class-export-global-declaration", "file", sourceFile.file, `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`, declaration.line));
669
646
  }
670
647
  return warnings;
671
648
  }
@@ -687,28 +664,13 @@ class AkanQualityScanner {
687
664
  if (declaration.exported) {
688
665
  if (isAllowedComponentExport(declaration, isPage))
689
666
  continue;
690
- warnings.push({
691
- rule: "akan.file.component-export",
692
- scope: "file",
693
- severity: "warning",
694
- file: sourceFile.file,
695
- line: declaration.line,
696
- message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
697
- });
667
+ warnings.push(fileWarning("akan.file.component-export", "file", sourceFile.file, `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`, declaration.line));
698
668
  continue;
699
669
  }
700
670
  if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
701
671
  continue;
702
- if (isRestrictedInternalKind(declaration.kind)) {
703
- warnings.push({
704
- rule: "akan.file.component-internal-declaration",
705
- scope: "file",
706
- severity: "warning",
707
- file: sourceFile.file,
708
- line: declaration.line,
709
- message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
710
- });
711
- }
672
+ if (isRestrictedInternalKind(declaration.kind))
673
+ warnings.push(fileWarning("akan.file.component-internal-declaration", "file", sourceFile.file, `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`, declaration.line));
712
674
  }
713
675
  return warnings;
714
676
  }
@@ -716,34 +678,15 @@ class AkanQualityScanner {
716
678
  const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
717
679
  if (!suffix)
718
680
  return [];
719
- const modelName = toPascalCase(path.basename(sourceFile.file, suffix));
720
- const warnings = [];
721
- for (const declaration of getTopLevelDeclarations(sourceFile)) {
722
- if (isAllowedConventionDeclaration(suffix, modelName, declaration))
723
- continue;
724
- warnings.push({
725
- rule: `akan.convention${suffix.replace(".ts", "")}`,
726
- scope: "convention",
727
- severity: "warning",
728
- file: sourceFile.file,
729
- line: declaration.line,
730
- message: `${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
731
- });
732
- }
733
- return warnings;
681
+ const modelName = toPascalCase(path2.basename(sourceFile.file, suffix));
682
+ return getTopLevelDeclarations(sourceFile).filter((declaration) => !isAllowedConventionDeclaration(suffix, modelName, declaration)).map((declaration) => fileWarning(`akan.convention${suffix.replace(".ts", "")}`, "convention", sourceFile.file, `${path2.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`, declaration.line));
734
683
  }
735
684
  #scanAbstractQuality({ file, content }) {
736
685
  const lineCount = AbstractDoc.lineCountOf(content);
737
686
  if (lineCount <= AbstractDoc.maxLines)
738
687
  return [];
739
688
  return [
740
- {
741
- rule: "akan.file.abstract-max-lines",
742
- scope: "file",
743
- severity: "warning",
744
- file,
745
- message: `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`
746
- }
689
+ fileWarning("akan.file.abstract-max-lines", "file", file, `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`)
747
690
  ];
748
691
  }
749
692
  #scanLayoutQuality(sourceFile) {
@@ -753,26 +696,12 @@ class AkanQualityScanner {
753
696
  const { type, name, isDir } = rootEntry;
754
697
  const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
755
698
  const kind = isDir ? "folder" : "file";
756
- if (!allowed) {
757
- warnings.push({
758
- rule: `akan.layout.${type}-root-${kind}`,
759
- scope: "layout",
760
- severity: "warning",
761
- file: sourceFile.file,
762
- message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`
763
- });
764
- }
699
+ if (!allowed)
700
+ warnings.push(fileWarning(`akan.layout.${type}-root-${kind}`, "layout", sourceFile.file, `Unexpected ${type} root ${kind} "${name}". ${rootEntryHintOf(type, name) ?? `Keep ${type} code in conventional ${type} folders.`}`));
765
701
  }
766
702
  const libFacetFile = getLibFacetRootFile(sourceFile.file);
767
- if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
768
- warnings.push({
769
- rule: "akan.layout.lib-facet-file",
770
- scope: "layout",
771
- severity: "warning",
772
- file: sourceFile.file,
773
- message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`
774
- });
775
- }
703
+ if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
704
+ warnings.push(fileWarning("akan.layout.lib-facet-file", "layout", sourceFile.file, `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`));
776
705
  const moduleUiWarning = getModuleUiWarning(sourceFile.file);
777
706
  if (moduleUiWarning)
778
707
  warnings.push(moduleUiWarning);
@@ -839,67 +768,48 @@ function formatQualityLocation(file, line) {
839
768
  function getExportedFunctionLikes(sourceFile) {
840
769
  const declarations = [];
841
770
  const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
771
+ const add = (name, kind, node, body, isEnumClass = false) => declarations.push({
772
+ name,
773
+ kind,
774
+ file: sourceFile.file,
775
+ line: getLine(sourceFile.sourceFile, node),
776
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
777
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass)
778
+ });
842
779
  for (const statement of sourceFile.sourceFile.statements) {
843
- if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
844
- declarations.push({
845
- name: statement.name.text,
846
- kind: "function",
847
- file: sourceFile.file,
848
- line: getLine(sourceFile.sourceFile, statement),
849
- bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
850
- duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
851
- });
852
- }
853
- if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement)) {
854
- declarations.push({
855
- name: statement.name.text,
856
- kind: "class",
857
- file: sourceFile.file,
858
- line: getLine(sourceFile.sourceFile, statement),
859
- bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
860
- duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
861
- });
862
- }
780
+ if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement))
781
+ add(statement.name.text, "function", statement, statement.body);
782
+ if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement))
783
+ add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
863
784
  if (ts3.isVariableStatement(statement) && isExported(statement)) {
864
785
  for (const declaration of statement.declarationList.declarations) {
865
786
  if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
866
787
  continue;
867
- declarations.push({
868
- name: declaration.name.text,
869
- kind: "function-variable",
870
- file: sourceFile.file,
871
- line: getLine(sourceFile.sourceFile, declaration),
872
- bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
873
- duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
874
- });
788
+ add(declaration.name.text, "function-variable", declaration, declaration.initializer);
875
789
  }
876
790
  }
877
791
  }
878
792
  return declarations;
879
793
  }
880
- function isPageRouteFile(file) {
794
+ function sysSegments(file) {
881
795
  const segments = file.split("/");
882
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
796
+ return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
797
+ }
798
+ function isPageRouteFile(file) {
799
+ return sysSegments(file)?.[2] === "page";
883
800
  }
884
801
  function isUiComponentFile(file) {
885
- const segments = file.split("/");
886
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
802
+ return sysSegments(file)?.[2] === "ui";
887
803
  }
888
804
  function isConventionDuplicateNameExempt(file, isEnumClass) {
889
- if (!isInLibModule(file))
805
+ if (!sysSegments(file)?.includes("lib"))
890
806
  return false;
891
- if (file.endsWith(".tsx"))
892
- return true;
893
- if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
807
+ if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file))
894
808
  return true;
895
809
  if (file.endsWith(".constant.ts"))
896
810
  return !isEnumClass;
897
811
  return false;
898
812
  }
899
- function isInLibModule(file) {
900
- const segments = file.split("/");
901
- return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
902
- }
903
813
  function isEnumClassStatement(sourceFile, statement) {
904
814
  if (!ts3.isClassDeclaration(statement))
905
815
  return false;
@@ -995,14 +905,7 @@ function isDefaultExportStatement(statement) {
995
905
  function getBangCommentWarnings(sourceFile) {
996
906
  if (!isClientReachableFile(sourceFile.file))
997
907
  return [];
998
- return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => ({
999
- rule: "akan.file.bang-comment-in-client",
1000
- scope: "file",
1001
- severity: "warning",
1002
- file: sourceFile.file,
1003
- line,
1004
- message: "A `//!` or `/*!` marker survives minification and ships to the browser."
1005
- }));
908
+ return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => fileWarning("akan.file.bang-comment-in-client", "file", sourceFile.file, "A `//!` or `/*!` marker survives minification and ships to the browser.", line));
1006
909
  }
1007
910
  function isClientReachableFile(file) {
1008
911
  if (/\.(test|spec)\.tsx?$/.test(file))
@@ -1021,61 +924,24 @@ function isClientReachableFile(file) {
1021
924
  function getPlaceholderExportWarnings(sourceFile) {
1022
925
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
1023
926
  return [];
1024
- return getTopLevelDeclarations(sourceFile).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
1025
- rule: "akan.file.placeholder-export",
1026
- scope: "file",
1027
- severity: "warning",
1028
- file: sourceFile.file,
1029
- line: declaration.line,
1030
- message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
1031
- }));
927
+ return getTopLevelDeclarations(sourceFile).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => fileWarning("akan.file.placeholder-export", "file", sourceFile.file, `Generated or barrel index file should not export placeholder "${declaration.name}".`, declaration.line));
1032
928
  }
1033
929
  function getDictionaryTextWarnings(sourceFile) {
1034
930
  if (!sourceFile.file.endsWith(".dictionary.ts"))
1035
931
  return [];
1036
932
  const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
1037
- return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile.content, pattern).map((line) => ({
1038
- rule: "akan.file.dictionary-stale-text",
1039
- scope: "file",
1040
- severity: "warning",
1041
- file: sourceFile.file,
1042
- line,
1043
- message: `Dictionary text appears to contain ${label}.`
1044
- })));
933
+ return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile.content, pattern).map((line) => fileWarning("akan.file.dictionary-stale-text", "file", sourceFile.file, `Dictionary text appears to contain ${label}.`, line)));
1045
934
  }
1046
935
  function getGlobalMutationWarnings(sourceFile) {
1047
936
  const warnings = [];
1048
937
  for (const statement of sourceFile.sourceFile.statements) {
1049
- if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
1050
- warnings.push({
1051
- rule: "akan.file.global-declaration",
1052
- scope: "file",
1053
- severity: "warning",
1054
- file: sourceFile.file,
1055
- line: getLine(sourceFile.sourceFile, statement),
1056
- message: "Global declarations require an explicit low-level integration allowlist."
1057
- });
1058
- }
1059
- if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
1060
- warnings.push({
1061
- rule: "akan.file.window-augmentation",
1062
- scope: "file",
1063
- severity: "warning",
1064
- file: sourceFile.file,
1065
- line: getLine(sourceFile.sourceFile, statement),
1066
- message: "Window augmentation should be isolated to approved browser integration files."
1067
- });
1068
- }
1069
- if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
1070
- warnings.push({
1071
- rule: "akan.file.prototype-mutation",
1072
- scope: "file",
1073
- severity: "warning",
1074
- file: sourceFile.file,
1075
- line: getLine(sourceFile.sourceFile, statement),
1076
- message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
1077
- });
1078
- }
938
+ const warn = (rule, message) => warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
939
+ if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
940
+ warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
941
+ if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window")
942
+ warn("akan.file.window-augmentation", "Window augmentation should be isolated to approved browser integration files.");
943
+ if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype."))
944
+ warn("akan.file.prototype-mutation", "Prototype mutation should be avoided or isolated to approved low-level integration files.");
1079
945
  }
1080
946
  return warnings;
1081
947
  }
@@ -1084,73 +950,59 @@ function getTopLevelDeclarations(sourceFile) {
1084
950
  }
1085
951
  function getTopLevelDeclaration(sourceFile, statement) {
1086
952
  const line = getLine(sourceFile, statement);
1087
- if (ts3.isClassDeclaration(statement) && statement.name) {
1088
- return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
1089
- }
1090
- if (ts3.isFunctionDeclaration(statement) && statement.name) {
1091
- return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
1092
- }
1093
- if (ts3.isInterfaceDeclaration(statement)) {
1094
- return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
1095
- }
1096
- if (ts3.isTypeAliasDeclaration(statement)) {
1097
- return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
1098
- }
1099
- if (ts3.isEnumDeclaration(statement)) {
1100
- return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
1101
- }
1102
- if (ts3.isVariableStatement(statement)) {
1103
- return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => ({
1104
- name: declaration.name.text,
1105
- kind: "variable",
1106
- line: getLine(sourceFile, declaration),
1107
- exported: isExported(statement),
1108
- node: statement
1109
- }));
1110
- }
1111
- if (ts3.isExportDeclaration(statement)) {
953
+ const declared = (name, kind, at = line) => ({
954
+ name,
955
+ kind,
956
+ line: at,
957
+ exported: isExported(statement),
958
+ node: statement
959
+ });
960
+ if (ts3.isClassDeclaration(statement) && statement.name)
961
+ return [declared(statement.name.text, "class")];
962
+ if (ts3.isFunctionDeclaration(statement) && statement.name)
963
+ return [declared(statement.name.text, "function")];
964
+ if (ts3.isInterfaceDeclaration(statement))
965
+ return [declared(statement.name.text, "interface")];
966
+ if (ts3.isTypeAliasDeclaration(statement))
967
+ return [declared(statement.name.text, "type")];
968
+ if (ts3.isEnumDeclaration(statement))
969
+ return [declared(statement.name.text, "enum")];
970
+ if (ts3.isVariableStatement(statement))
971
+ return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => declared(declaration.name.text, "variable", getLine(sourceFile, declaration)));
972
+ if (ts3.isExportDeclaration(statement))
1112
973
  return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
1113
- }
1114
974
  return [];
1115
975
  }
1116
- function isAllowedConventionDeclaration(suffix, modelName, declaration) {
1117
- if (suffix === ".dictionary.ts")
1118
- return isExportedConst(declaration) && declaration.name === "dictionary";
976
+ function getConventionClassNames(suffix, model) {
1119
977
  if (suffix === ".constant.ts")
1120
- return isAllowedConstantDeclaration(modelName, declaration);
978
+ return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
1121
979
  if (suffix === ".document.ts")
1122
- return declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name);
980
+ return [`${model}Filter`, model, `${model}Model`];
1123
981
  if (suffix === ".service.ts")
1124
- return declaration.kind === "class" && declaration.name === `${modelName}Service`;
982
+ return [`${model}Service`];
1125
983
  if (suffix === ".signal.ts")
1126
- return declaration.kind === "class" && [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name);
984
+ return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
1127
985
  if (suffix === ".store.ts")
1128
- return declaration.kind === "class" && declaration.name === `${modelName}Store`;
1129
- return false;
986
+ return [`${model}Store`];
987
+ return [];
1130
988
  }
1131
- function isAllowedConstantDeclaration(modelName, declaration) {
989
+ function isAllowedConventionDeclaration(suffix, modelName, declaration) {
990
+ if (suffix === ".dictionary.ts")
991
+ return isExportedConst(declaration) && declaration.name === "dictionary";
1132
992
  if (declaration.kind !== "class")
1133
993
  return false;
1134
- if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
994
+ if (getConventionClassNames(suffix, modelName).includes(declaration.name))
1135
995
  return true;
1136
- if (!ts3.isClassDeclaration(declaration.node))
1137
- return false;
1138
- const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
1139
- const expression = heritageClause?.types[0]?.expression;
1140
- return !!expression && expression.getText().startsWith("enumOf(");
996
+ return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
1141
997
  }
1142
998
  function getConventionDescription(suffix, modelName) {
1143
999
  if (suffix === ".dictionary.ts")
1144
1000
  return "export const dictionary";
1145
- if (suffix === ".constant.ts")
1146
- return `${modelName}Input, ${modelName}Object, ${modelName}, Light${modelName}, ${modelName}Insight, or enumOf classes`;
1147
- if (suffix === ".document.ts")
1148
- return `${modelName}Filter, ${modelName}, or ${modelName}Model`;
1149
- if (suffix === ".service.ts")
1150
- return `${modelName}Service`;
1151
- if (suffix === ".signal.ts")
1152
- return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
1153
- return `${modelName}Store`;
1001
+ const names = [
1002
+ ...getConventionClassNames(suffix, modelName),
1003
+ ...suffix === ".constant.ts" ? ["enumOf classes"] : []
1004
+ ];
1005
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
1154
1006
  }
1155
1007
  var sysRootTypes = { apps: "app", libs: "lib" };
1156
1008
  function getSysRootEntry(file) {
@@ -1182,13 +1034,7 @@ function getModuleUiWarning(file) {
1182
1034
  const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
1183
1035
  if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
1184
1036
  return null;
1185
- return {
1186
- rule: "akan.layout.module-ui-file",
1187
- scope: "layout",
1188
- severity: "warning",
1189
- file,
1190
- message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
1191
- };
1037
+ return fileWarning("akan.layout.module-ui-file", "layout", file, `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`);
1192
1038
  }
1193
1039
  function getModuleInfo(file) {
1194
1040
  const segments = file.split("/");
@@ -1204,10 +1050,8 @@ function getModuleInfo(file) {
1204
1050
  if (segments.length !== libIndex + 3)
1205
1051
  return null;
1206
1052
  const fileName = segments[libIndex + 2];
1207
- if (moduleName.startsWith("__")) {
1208
- const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
1209
- return { moduleName: scalarName, fileName, kind: "scalar" };
1210
- }
1053
+ if (moduleName.startsWith("__"))
1054
+ return { moduleName, fileName, kind: "scalar" };
1211
1055
  if (moduleName.startsWith("_"))
1212
1056
  return { moduleName, fileName, kind: "service" };
1213
1057
  return { moduleName, fileName, kind: "database" };
@@ -1216,7 +1060,7 @@ function isExportedConst(declaration) {
1216
1060
  return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
1217
1061
  }
1218
1062
  function isExported(node) {
1219
- return !!ts3.getCombinedModifierFlags(node) && !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
1063
+ return !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
1220
1064
  }
1221
1065
  function isFunctionLikeInitializer(node) {
1222
1066
  return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
@@ -1262,7 +1106,7 @@ function toPascalCase(value) {
1262
1106
  return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
1263
1107
  }
1264
1108
  function toPosix(value) {
1265
- return value.split(path.sep).join("/");
1109
+ return value.split(path2.sep).join("/");
1266
1110
  }
1267
1111
  function groupBy(items, getKey) {
1268
1112
  const grouped = new Map;
@@ -1279,24 +1123,16 @@ function compareWarnings(a, b) {
1279
1123
  // pkgs/@akanjs/cli/quality/quality.script.ts
1280
1124
  import { Logger } from "akanjs/common";
1281
1125
 
1282
- // pkgs/@akanjs/cli/quality/quality.runner.ts
1283
- class QualityRunner extends runner("quality") {
1284
- async scan(workspace) {
1285
- return await new AkanQualityScanner().scan(workspace.workspaceRoot);
1286
- }
1287
- }
1288
-
1289
- // pkgs/@akanjs/cli/quality/quality.script.ts
1290
- class QualityScript extends script("quality", [QualityRunner]) {
1126
+ class QualityScript extends script("quality") {
1291
1127
  async scan(workspace, format = "text") {
1292
1128
  const spinner = workspace.spinning("Scanning Akan code quality...");
1293
- const result = await this.qualityRunner.scan(workspace);
1129
+ const result = await new AkanQualityScanner().scan(workspace.workspaceRoot);
1294
1130
  spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
1295
1131
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
1296
1132
  }
1297
1133
  async ssr(workspace, format = "text") {
1298
1134
  const spinner = workspace.spinning("Measuring Akan server/client render balance...");
1299
- const scanned = await this.qualityRunner.scan(workspace);
1135
+ const scanned = await new AkanQualityScanner().scan(workspace.workspaceRoot);
1300
1136
  const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
1301
1137
  spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
1302
1138
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
@@ -1305,15 +1141,7 @@ class QualityScript extends script("quality", [QualityRunner]) {
1305
1141
 
1306
1142
  // pkgs/@akanjs/cli/quality/quality.command.ts
1307
1143
  class QualityCommand extends command("quality", [QualityScript], ({ public: target }) => ({
1308
- quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
1309
- desc: "quality action",
1310
- default: "scan",
1311
- enum: ["scan", "ssr"]
1312
- }).option("format", String, {
1313
- desc: "output format",
1314
- default: "text",
1315
- enum: ["text", "json"]
1316
- }).with(Workspace).exec(async function(action, format, workspace) {
1144
+ quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, { desc: "quality action", default: "scan", enum: ["scan", "ssr"] }).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).with(Workspace).exec(async function(action, format, workspace) {
1317
1145
  if (action === "scan")
1318
1146
  await this.qualityScript.scan(workspace, format);
1319
1147
  else if (action === "ssr")