@akanjs/cli 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 (187) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-awv258wb.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-1nqh35ez.js +1457 -0
  6. package/README.md +3 -0
  7. package/TunnelShare-scf4e4pd.js +9 -0
  8. package/{agent.command-d2gfxfck.js → agent.command-1s9ag7sb.js} +8 -9
  9. package/akanCode.js +102 -0
  10. package/{application.command-pj2m7584.js → application.command-49hwy6va.js} +51 -78
  11. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-ybyp97pv.js} +63 -64
  12. package/{applicationReleasePackager-4yasxwv9.js → applicationReleasePackager-esnrphw8.js} +40 -60
  13. package/buildBatch.proc.js +19 -24
  14. package/{capacitorApp-36rbr858.js → capacitorApp-1synz4en.js} +3 -4
  15. package/cloud.command-d7rqtzt9.js +94 -0
  16. package/cloud.script-bkbpcr96.js +19 -0
  17. package/code.command-nn48xzej.js +154 -0
  18. package/commandManifest.json +1 -1
  19. package/consoleToStderr-h5bryj94.js +7 -0
  20. package/context.command-s3mcmvpa.js +48 -0
  21. package/context.runner-k99nb5pk.js +21 -0
  22. package/{dependencyScanner-shs82ynp.js → dependencyScanner-ye2e6pdz.js} +2 -2
  23. package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
  24. package/fileSys-5ngz93ga.js +9 -0
  25. package/guideline.command-sct4qnt5.js +42 -0
  26. package/guidelines/agentRule/agentRule.instruction.md +206 -25
  27. package/guidelines/conventions/conventions.instruction.md +79 -24
  28. package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
  29. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  30. package/guidelines/queryRule/queryRule.instruction.md +17 -7
  31. package/guidelines/runtimeRule/runtimeRule.instruction.md +74 -0
  32. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  33. package/guidelines/transportRule/transportRule.instruction.md +51 -0
  34. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  35. package/incrementalBuilder.proc.js +31 -48
  36. package/{index-h7yw2z9n.js → index-0bq14vr1.js} +8 -14
  37. package/index-13ffe9wp.js +69 -0
  38. package/index-22d57v4c.js +266 -0
  39. package/{index-fz5q3gd7.js → index-31yr441d.js} +10 -17
  40. package/index-3hx1z96c.js +87 -0
  41. package/index-3sq321ak.js +53 -0
  42. package/{index-kcxmv48c.js → index-3t7g9e4r.js} +45 -79
  43. package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
  44. package/{index-47pj3aee.js → index-5e0n4hqx.js} +11 -12
  45. package/{index-259dsnz3.js → index-5pc1qrbv.js} +5 -8
  46. package/index-7md2g9vw.js +6 -0
  47. package/index-8mna0947.js +187 -0
  48. package/{index-zry5nz81.js → index-8sf7wmdr.js} +51 -30
  49. package/{index-2tv3fdx4.js → index-9d9bgcx4.js} +342 -765
  50. package/index-9dp031by.js +12 -0
  51. package/{index-ne2qy0nk.js → index-ahemdr5n.js} +12 -12
  52. package/{index-ws5e0j51.js → index-as7xmyz8.js} +40 -24
  53. package/index-axzjaqve.js +1118 -0
  54. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  55. package/index-c6eggbee.js +123 -0
  56. package/index-d2242ksy.js +99 -0
  57. package/{index-a64esmtb.js → index-ddxvdd1p.js} +169 -305
  58. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  59. package/{index-pnqbkf5p.js → index-dzf3r49a.js} +6 -2
  60. package/{index-w8kmx1b6.js → index-fgq75haa.js} +198 -290
  61. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  62. package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
  63. package/{index-wt7qrtd4.js → index-mxav6j3c.js} +93 -232
  64. package/{index-gyd220p2.js → index-myhc3c1j.js} +554 -577
  65. package/{index-600zhnve.js → index-nv5976x3.js} +56 -197
  66. package/index-p6pbz9n5.js +25 -0
  67. package/{index-4edzf1gr.js → index-przybvy5.js} +77 -150
  68. package/index-smme4r47.js +49 -0
  69. package/{index-mak3cd28.js → index-w2zdcw3k.js} +137 -255
  70. package/index-wa5jbxj1.js +760 -0
  71. package/{index-v0qh106m.js → index-ww4z397j.js} +527 -723
  72. package/{index-j9yhg404.js → index-xqjwd384.js} +4 -7
  73. package/{index-7737gxcp.js → index-xqpzbab5.js} +107 -241
  74. package/index-xsjh6nzh.js +2729 -0
  75. package/{index-ktb5njgy.js → index-ygn77a5w.js} +47 -99
  76. package/index-ztp3e73t.js +700 -0
  77. package/index.js +23 -22
  78. package/{library.command-nbsky672.js → library.command-1gx04jgn.js} +4 -5
  79. package/{localRegistry.command-yeq57wjr.js → localRegistry.command-zndxj1q6.js} +22 -42
  80. package/{module.command-29vqzrh8.js → module.command-3waddk95.js} +8 -11
  81. package/{package.command-axv0abmn.js → package.command-zrc7br1y.js} +6 -7
  82. package/package.json +14 -5
  83. package/{page.command-vs9s3zrh.js → page.command-vpjgardh.js} +4 -5
  84. package/{primitive.command-cbxmqbtc.js → primitive.command-sz84tk4t.js} +9 -22
  85. package/prompter-ptaa3nk0.js +8 -0
  86. package/{quality.command-gmbs7nws.js → quality.command-g869hafs.js} +203 -376
  87. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  88. package/{repair.command-bqsjptsj.js → repair.command-chsnjvvv.js} +11 -35
  89. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  90. package/{scalar.command-6k1vhe9s.js → scalar.command-2v21newx.js} +7 -10
  91. package/skills/akan-code/SKILL.md +130 -0
  92. package/skills/akan-debug/SKILL.md +42 -0
  93. package/skills/akan-deps/SKILL.md +50 -0
  94. package/skills/akan-err/SKILL.md +39 -0
  95. package/skills/akan-module/SKILL.md +45 -0
  96. package/skills/akan-scaffold/SKILL.md +76 -0
  97. package/skills/akan-scalar/SKILL.md +52 -0
  98. package/skills/akan-service/SKILL.md +34 -0
  99. package/skills/akan-store/SKILL.md +67 -0
  100. package/skills/akan-ui/SKILL.md +63 -0
  101. package/skills/akan-validate/SKILL.md +44 -0
  102. package/{subspace.command-fwjfcts7.js → subspace.command-ckcbwfp1.js} +138 -67
  103. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  104. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  105. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  106. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  107. package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
  108. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  109. package/templates/appSample/lib/task/task.signal.ts +1 -1
  110. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  111. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  112. package/templates/client.ts +7 -30
  113. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  114. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  115. package/templates/crudPages/new/page.tsx +2 -1
  116. package/templates/crudPages/page.tsx +2 -1
  117. package/templates/crudSinglePage/page.tsx +2 -1
  118. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  119. package/templates/lib/__lib/lib.document.ts +0 -2
  120. package/templates/lib/sig.ts +8 -30
  121. package/templates/localDev/docker-compose.yaml.template +3 -14
  122. package/templates/module/__model__.signal.ts +12 -4
  123. package/templates/pkgRoot/tsconfig.json.template +2 -7
  124. package/templates/server.ts +0 -2
  125. package/templates/service/__model__.abstract.md.template +8 -0
  126. package/tunnel.command-kd2yr4fx.js +129 -0
  127. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  128. package/typecheck.proc.js +1 -1
  129. package/workflow.command-44252cqv.js +79 -0
  130. package/{workspace.command-8s050m0h.js → workspace.command-fqcztt5p.js} +77 -109
  131. package/abstractCompactor-mjh5ppsq.js +0 -101
  132. package/cloud.command-jshwdq22.js +0 -93
  133. package/context.command-ez8a0zyz.js +0 -80
  134. package/devTuiView-c08ccbat.js +0 -9
  135. package/getDirname-dr3jz1xg.js +0 -7
  136. package/guideline.command-xmqwx8zm.js +0 -359
  137. package/guidelines/agentRule/agentRule.generate.json +0 -31
  138. package/guidelines/componentRule/componentRule.generate.json +0 -31
  139. package/guidelines/conventions/conventions.generate.json +0 -25
  140. package/guidelines/cssRule/cssRule.generate.json +0 -31
  141. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  142. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  143. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  144. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  145. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  146. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  147. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  148. package/guidelines/framework/framework.generate.json +0 -32
  149. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  150. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  151. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  152. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  153. package/guidelines/modelService/modelService.generate.json +0 -31
  154. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  155. package/guidelines/modelStore/modelStore.generate.json +0 -31
  156. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  157. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  158. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  159. package/guidelines/modelView/modelView.generate.json +0 -31
  160. package/guidelines/modelZone/modelZone.generate.json +0 -31
  161. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  162. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  163. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  164. package/guidelines/queryRule/queryRule.generate.json +0 -31
  165. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  166. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  167. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  168. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  169. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  170. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  171. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  172. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  173. package/guidelines/transportRule/transportRule.generate.json +0 -31
  174. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  175. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  176. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  177. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  178. package/index-2hq52jq3.js +0 -465
  179. package/index-6wwzs1bc.js +0 -7
  180. package/index-8pm30m73.js +0 -841
  181. package/index-kn1h7p1x.js +0 -560
  182. package/index-meh558jy.js +0 -65
  183. package/index-rwg2aa4f.js +0 -229
  184. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  185. package/templates/module/__model__.abstract.md +0 -28
  186. package/templates/service/__model__.abstract.md +0 -28
  187. package/workflow.command-nwpcdc8p.js +0 -107
@@ -1,29 +1,88 @@
1
1
  // @bun
2
2
  import {
3
3
  Workspace,
4
- runner,
5
4
  script,
6
5
  command
7
- } from "./index-mak3cd28.js";
8
- import"./index-q0y2jq9j.js";
6
+ } from "./index-w2zdcw3k.js";
7
+ import"./index-5b6tavv3.js";
9
8
  import {
10
9
  rootAllowedFiles,
11
10
  rootAllowedDirs,
12
11
  isAllowedLibFacetRootFile
13
- } from "./index-2tv3fdx4.js";
14
- import"./index-6wwzs1bc.js";
15
- import {
16
- AbstractDoc
17
- } from "./index-meh558jy.js";
12
+ } from "./index-9d9bgcx4.js";
18
13
 
19
14
  // pkgs/@akanjs/devkit/qualityScanner.ts
20
15
  import { createHash } from "crypto";
21
16
  import { readdir, readFile, stat } from "fs/promises";
22
- import path from "path";
17
+ import path2 from "path";
23
18
  import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
24
19
  import ignore from "ignore";
25
20
  import ts3 from "typescript";
26
21
 
22
+ // pkgs/@akanjs/devkit/abstractDoc.ts
23
+ import path from "path";
24
+
25
+ class AbstractDoc {
26
+ path;
27
+ content;
28
+ siblingFiles;
29
+ static suffix = ".abstract.md";
30
+ static maxLines = 300;
31
+ static #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
32
+ static isAbstractPath(filePath) {
33
+ return filePath.endsWith(AbstractDoc.suffix);
34
+ }
35
+ static lineCountOf(content) {
36
+ return content.split(/\r?\n/).length;
37
+ }
38
+ static kindOf(filePath) {
39
+ const [root, folder, ...rest] = filePath.split("/");
40
+ if (root !== "lib" || !folder)
41
+ return "other";
42
+ if (folder === "__scalar")
43
+ return rest.length === 2 ? "scalar" : "other";
44
+ if (rest.length !== 1)
45
+ return "other";
46
+ return folder.startsWith("_") ? "service" : "domain";
47
+ }
48
+ static async findAll(sys, { module } = {}) {
49
+ const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`)).map((filePath) => filePath.split(path.sep).join("/")).sort();
50
+ const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
51
+ if (!module)
52
+ return docs;
53
+ return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
54
+ }
55
+ static async read(sys, filePath) {
56
+ const dirPath = filePath.split("/").slice(0, -1).join("/");
57
+ const [content, folderFiles] = await Promise.all([sys.readFile(filePath), sys.readdir(dirPath)]);
58
+ return new AbstractDoc(filePath, content, folderFiles.filter((fileName) => !AbstractDoc.isAbstractPath(fileName)));
59
+ }
60
+ constructor(path2, content, siblingFiles) {
61
+ this.path = path2;
62
+ this.content = content;
63
+ this.siblingFiles = siblingFiles;
64
+ }
65
+ get kind() {
66
+ return AbstractDoc.kindOf(this.path);
67
+ }
68
+ get folderName() {
69
+ return this.path.split("/").at(-2) ?? "";
70
+ }
71
+ get moduleName() {
72
+ return (this.path.split("/").at(-1) ?? "").slice(0, -AbstractDoc.suffix.length);
73
+ }
74
+ get lineCount() {
75
+ return AbstractDoc.lineCountOf(this.content);
76
+ }
77
+ canReplaceWith(candidate) {
78
+ const next = candidate.trim();
79
+ if (!next.startsWith("#"))
80
+ return false;
81
+ const lineCount = AbstractDoc.lineCountOf(next);
82
+ return lineCount >= 3 && lineCount < this.lineCount;
83
+ }
84
+ }
85
+
27
86
  // pkgs/@akanjs/devkit/formSetterScanner.ts
28
87
  import ts from "typescript";
29
88
 
@@ -99,25 +158,11 @@ var STATIC_COMPONENT_MIN_MASS = 4;
99
158
  var MIXED_COMPONENT_MIN_MASS = 10;
100
159
  var MIXED_COMPONENT_MAX_TOUCHES = 2;
101
160
  var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
161
+ var fileWarning = (rule, scope, file, message, line) => line === undefined ? { rule, scope, severity: "warning", file, message } : { rule, scope, severity: "warning", file, line, message };
102
162
 
103
163
  class SsrScanner {
104
164
  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
- ]);
165
+ static #clientGlobals = new Set("window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver".split(" ").concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")));
121
166
  static #clientRuntimeImports = new Set(["st", "fetch"]);
122
167
  scan(sourceFiles) {
123
168
  const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
@@ -138,38 +183,17 @@ class SsrScanner {
138
183
  const components = this.#getComponents(sourceFile, vendorNames);
139
184
  const warnings = [];
140
185
  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
- });
186
+ 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
187
  }
150
188
  for (const component of components) {
151
189
  if (component.vendorTags)
152
190
  continue;
153
191
  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
- });
192
+ 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
193
  continue;
163
194
  }
164
195
  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
- });
196
+ 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
197
  }
174
198
  }
175
199
  warnings.push(...this.#getMountLoadWarnings(sourceFile));
@@ -189,32 +213,16 @@ class SsrScanner {
189
213
  entry.serverFiles += 1;
190
214
  modules.set(moduleDir, entry);
191
215
  }
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
- }));
216
+ 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
217
  }
200
218
  #getMountLoadWarnings(sourceFile) {
201
219
  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);
220
+ SsrScanner.#walk(sourceFile.sourceFile, (node) => {
221
+ if (!this.#isMountEffect(sourceFile.sourceFile, node))
222
+ return;
223
+ for (const load of this.#getLoadCalls(sourceFile.sourceFile, node))
224
+ 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)));
225
+ });
218
226
  return warnings;
219
227
  }
220
228
  #isMountEffect(sourceFile, node) {
@@ -228,35 +236,23 @@ class SsrScanner {
228
236
  }
229
237
  #getLoadCalls(sourceFile, node) {
230
238
  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);
239
+ SsrScanner.#walk(node, (child) => {
240
+ if (!ts2.isCallExpression(child))
241
+ return;
242
+ const callee = child.expression.getText(sourceFile);
243
+ if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
244
+ calls.push({ callee, node: child });
245
+ });
240
246
  return calls;
241
247
  }
242
248
  #getTemplateStateWarnings(sourceFile) {
243
249
  if (!sourceFile.file.endsWith(".Template.tsx"))
244
250
  return [];
245
251
  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);
252
+ SsrScanner.#walk(sourceFile.sourceFile, (node) => {
253
+ if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
254
+ 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)));
255
+ });
260
256
  return warnings;
261
257
  }
262
258
  #measureBalance(sourceFiles) {
@@ -311,7 +307,7 @@ class SsrScanner {
311
307
  }
312
308
  #getTouches(sourceFile, node) {
313
309
  const touches = [];
314
- const visit = (child) => {
310
+ SsrScanner.#walk(node, (child) => {
315
311
  if (ts2.isCallExpression(child)) {
316
312
  const callee = child.expression.getText(sourceFile);
317
313
  const bareName = callee.split(".").pop() ?? callee;
@@ -329,31 +325,32 @@ class SsrScanner {
329
325
  else if (SsrScanner.#clientGlobals.has(root))
330
326
  touches.push(root);
331
327
  }
332
- ts2.forEachChild(child, visit);
333
- };
334
- ts2.forEachChild(node, visit);
328
+ });
335
329
  return touches;
336
330
  }
337
331
  #getTagNames(sourceFile, node) {
338
332
  const tags = new Set;
339
- const visit = (child) => {
333
+ SsrScanner.#walk(node, (child) => {
340
334
  if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
341
335
  tags.add(child.tagName.getText(sourceFile).split(".")[0]);
342
- ts2.forEachChild(child, visit);
343
- };
344
- ts2.forEachChild(node, visit);
336
+ });
345
337
  return tags;
346
338
  }
347
339
  #getMass(node) {
348
340
  let mass = 0;
349
- const visit = (child) => {
341
+ SsrScanner.#walk(node, (child) => {
350
342
  if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
351
343
  mass += 1;
352
- ts2.forEachChild(child, visit);
353
- };
354
- ts2.forEachChild(node, visit);
344
+ });
355
345
  return mass;
356
346
  }
347
+ static #walk(node, visit) {
348
+ const walk = (child) => {
349
+ visit(child);
350
+ ts2.forEachChild(child, walk);
351
+ };
352
+ ts2.forEachChild(node, walk);
353
+ }
357
354
  #hasUseClient(sourceFile) {
358
355
  const first = sourceFile.statements[0];
359
356
  if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
@@ -491,7 +488,7 @@ var RULE_FIXES = {
491
488
  "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
492
489
  "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
493
490
  "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.",
491
+ "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
492
  "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
496
493
  "akan.file.bang-comment-in-client": "Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
497
494
  "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
@@ -549,7 +546,7 @@ class AkanQualityScanner {
549
546
  const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
550
547
  const files = [];
551
548
  for (const targetRoot of ["apps", "libs"]) {
552
- const absoluteTargetRoot = path.join(workspaceRoot, targetRoot);
549
+ const absoluteTargetRoot = path2.join(workspaceRoot, targetRoot);
553
550
  if (!await isDirectory(absoluteTargetRoot))
554
551
  continue;
555
552
  await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
@@ -557,7 +554,7 @@ class AkanQualityScanner {
557
554
  return files.sort();
558
555
  }
559
556
  async#readGitIgnore(workspaceRoot) {
560
- const gitIgnorePath = path.join(workspaceRoot, ".gitignore");
557
+ const gitIgnorePath = path2.join(workspaceRoot, ".gitignore");
561
558
  if (!await Bun.file(gitIgnorePath).exists())
562
559
  return [];
563
560
  return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
@@ -565,8 +562,8 @@ class AkanQualityScanner {
565
562
  async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
566
563
  const entries = await readdir(currentPath, { withFileTypes: true });
567
564
  for (const entry of entries) {
568
- const absolutePath = path.join(currentPath, entry.name);
569
- const relativePath = toPosix(path.relative(workspaceRoot, absolutePath));
565
+ const absolutePath = path2.join(currentPath, entry.name);
566
+ const relativePath = toPosix(path2.relative(workspaceRoot, absolutePath));
570
567
  if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
571
568
  continue;
572
569
  if (entry.isDirectory()) {
@@ -581,7 +578,7 @@ class AkanQualityScanner {
581
578
  }
582
579
  }
583
580
  async#readSourceFile(workspaceRoot, file) {
584
- const absolutePath = path.join(workspaceRoot, file);
581
+ const absolutePath = path2.join(workspaceRoot, file);
585
582
  const content = await readFile(absolutePath, "utf8");
586
583
  return {
587
584
  file,
@@ -591,7 +588,7 @@ class AkanQualityScanner {
591
588
  };
592
589
  }
593
590
  async#readTextFile(workspaceRoot, file) {
594
- return { file, content: await readFile(path.join(workspaceRoot, file), "utf8") };
591
+ return { file, content: await readFile(path2.join(workspaceRoot, file), "utf8") };
595
592
  }
596
593
  #scanGlobalQuality(sourceFiles) {
597
594
  const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
@@ -627,24 +624,10 @@ class AkanQualityScanner {
627
624
  const warnings = [];
628
625
  const lineCount = sourceFile.content.split(/\r?\n/).length;
629
626
  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
- }
627
+ if (recommendedLineLimit && lineCount > recommendedLineLimit)
628
+ warnings.push(fileWarning("akan.file.recommended-max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`));
629
+ if (lineCount > MAX_FILE_LINES)
630
+ warnings.push(fileWarning("akan.file.max-lines", "file", sourceFile.file, `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`));
648
631
  warnings.push(...getBangCommentWarnings(sourceFile));
649
632
  warnings.push(...getPlaceholderExportWarnings(sourceFile));
650
633
  warnings.push(...getDictionaryTextWarnings(sourceFile));
@@ -658,14 +641,7 @@ class AkanQualityScanner {
658
641
  continue;
659
642
  if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
660
643
  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
- });
644
+ 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
645
  }
670
646
  return warnings;
671
647
  }
@@ -687,28 +663,13 @@ class AkanQualityScanner {
687
663
  if (declaration.exported) {
688
664
  if (isAllowedComponentExport(declaration, isPage))
689
665
  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
- });
666
+ 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
667
  continue;
699
668
  }
700
669
  if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
701
670
  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
- }
671
+ if (isRestrictedInternalKind(declaration.kind))
672
+ 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
673
  }
713
674
  return warnings;
714
675
  }
@@ -716,34 +677,15 @@ class AkanQualityScanner {
716
677
  const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
717
678
  if (!suffix)
718
679
  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;
680
+ const modelName = toPascalCase(path2.basename(sourceFile.file, suffix));
681
+ 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
682
  }
735
683
  #scanAbstractQuality({ file, content }) {
736
684
  const lineCount = AbstractDoc.lineCountOf(content);
737
685
  if (lineCount <= AbstractDoc.maxLines)
738
686
  return [];
739
687
  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
- }
688
+ fileWarning("akan.file.abstract-max-lines", "file", file, `Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`)
747
689
  ];
748
690
  }
749
691
  #scanLayoutQuality(sourceFile) {
@@ -753,26 +695,12 @@ class AkanQualityScanner {
753
695
  const { type, name, isDir } = rootEntry;
754
696
  const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
755
697
  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
- }
698
+ if (!allowed)
699
+ warnings.push(fileWarning(`akan.layout.${type}-root-${kind}`, "layout", sourceFile.file, `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`));
765
700
  }
766
701
  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
- }
702
+ if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
703
+ 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
704
  const moduleUiWarning = getModuleUiWarning(sourceFile.file);
777
705
  if (moduleUiWarning)
778
706
  warnings.push(moduleUiWarning);
@@ -839,67 +767,48 @@ function formatQualityLocation(file, line) {
839
767
  function getExportedFunctionLikes(sourceFile) {
840
768
  const declarations = [];
841
769
  const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
770
+ const add = (name, kind, node, body, isEnumClass = false) => declarations.push({
771
+ name,
772
+ kind,
773
+ file: sourceFile.file,
774
+ line: getLine(sourceFile.sourceFile, node),
775
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
776
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass)
777
+ });
842
778
  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
- }
779
+ if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement))
780
+ add(statement.name.text, "function", statement, statement.body);
781
+ if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement))
782
+ add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
863
783
  if (ts3.isVariableStatement(statement) && isExported(statement)) {
864
784
  for (const declaration of statement.declarationList.declarations) {
865
785
  if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
866
786
  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
- });
787
+ add(declaration.name.text, "function-variable", declaration, declaration.initializer);
875
788
  }
876
789
  }
877
790
  }
878
791
  return declarations;
879
792
  }
880
- function isPageRouteFile(file) {
793
+ function sysSegments(file) {
881
794
  const segments = file.split("/");
882
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
795
+ return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
796
+ }
797
+ function isPageRouteFile(file) {
798
+ return sysSegments(file)?.[2] === "page";
883
799
  }
884
800
  function isUiComponentFile(file) {
885
- const segments = file.split("/");
886
- return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
801
+ return sysSegments(file)?.[2] === "ui";
887
802
  }
888
803
  function isConventionDuplicateNameExempt(file, isEnumClass) {
889
- if (!isInLibModule(file))
804
+ if (!sysSegments(file)?.includes("lib"))
890
805
  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"))
806
+ if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file))
894
807
  return true;
895
808
  if (file.endsWith(".constant.ts"))
896
809
  return !isEnumClass;
897
810
  return false;
898
811
  }
899
- function isInLibModule(file) {
900
- const segments = file.split("/");
901
- return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
902
- }
903
812
  function isEnumClassStatement(sourceFile, statement) {
904
813
  if (!ts3.isClassDeclaration(statement))
905
814
  return false;
@@ -995,14 +904,7 @@ function isDefaultExportStatement(statement) {
995
904
  function getBangCommentWarnings(sourceFile) {
996
905
  if (!isClientReachableFile(sourceFile.file))
997
906
  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
- }));
907
+ 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
908
  }
1007
909
  function isClientReachableFile(file) {
1008
910
  if (/\.(test|spec)\.tsx?$/.test(file))
@@ -1021,61 +923,24 @@ function isClientReachableFile(file) {
1021
923
  function getPlaceholderExportWarnings(sourceFile) {
1022
924
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
1023
925
  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
- }));
926
+ 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
927
  }
1033
928
  function getDictionaryTextWarnings(sourceFile) {
1034
929
  if (!sourceFile.file.endsWith(".dictionary.ts"))
1035
930
  return [];
1036
931
  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
- })));
932
+ 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
933
  }
1046
934
  function getGlobalMutationWarnings(sourceFile) {
1047
935
  const warnings = [];
1048
936
  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
- }
937
+ const warn = (rule, message) => warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
938
+ if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
939
+ warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
940
+ if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window")
941
+ warn("akan.file.window-augmentation", "Window augmentation should be isolated to approved browser integration files.");
942
+ if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype."))
943
+ warn("akan.file.prototype-mutation", "Prototype mutation should be avoided or isolated to approved low-level integration files.");
1079
944
  }
1080
945
  return warnings;
1081
946
  }
@@ -1084,73 +949,59 @@ function getTopLevelDeclarations(sourceFile) {
1084
949
  }
1085
950
  function getTopLevelDeclaration(sourceFile, statement) {
1086
951
  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)) {
952
+ const declared = (name, kind, at = line) => ({
953
+ name,
954
+ kind,
955
+ line: at,
956
+ exported: isExported(statement),
957
+ node: statement
958
+ });
959
+ if (ts3.isClassDeclaration(statement) && statement.name)
960
+ return [declared(statement.name.text, "class")];
961
+ if (ts3.isFunctionDeclaration(statement) && statement.name)
962
+ return [declared(statement.name.text, "function")];
963
+ if (ts3.isInterfaceDeclaration(statement))
964
+ return [declared(statement.name.text, "interface")];
965
+ if (ts3.isTypeAliasDeclaration(statement))
966
+ return [declared(statement.name.text, "type")];
967
+ if (ts3.isEnumDeclaration(statement))
968
+ return [declared(statement.name.text, "enum")];
969
+ if (ts3.isVariableStatement(statement))
970
+ return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => declared(declaration.name.text, "variable", getLine(sourceFile, declaration)));
971
+ if (ts3.isExportDeclaration(statement))
1112
972
  return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
1113
- }
1114
973
  return [];
1115
974
  }
1116
- function isAllowedConventionDeclaration(suffix, modelName, declaration) {
1117
- if (suffix === ".dictionary.ts")
1118
- return isExportedConst(declaration) && declaration.name === "dictionary";
975
+ function getConventionClassNames(suffix, model) {
1119
976
  if (suffix === ".constant.ts")
1120
- return isAllowedConstantDeclaration(modelName, declaration);
977
+ return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
1121
978
  if (suffix === ".document.ts")
1122
- return declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name);
979
+ return [`${model}Filter`, model, `${model}Model`];
1123
980
  if (suffix === ".service.ts")
1124
- return declaration.kind === "class" && declaration.name === `${modelName}Service`;
981
+ return [`${model}Service`];
1125
982
  if (suffix === ".signal.ts")
1126
- return declaration.kind === "class" && [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name);
983
+ return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
1127
984
  if (suffix === ".store.ts")
1128
- return declaration.kind === "class" && declaration.name === `${modelName}Store`;
1129
- return false;
985
+ return [`${model}Store`];
986
+ return [];
1130
987
  }
1131
- function isAllowedConstantDeclaration(modelName, declaration) {
988
+ function isAllowedConventionDeclaration(suffix, modelName, declaration) {
989
+ if (suffix === ".dictionary.ts")
990
+ return isExportedConst(declaration) && declaration.name === "dictionary";
1132
991
  if (declaration.kind !== "class")
1133
992
  return false;
1134
- if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
993
+ if (getConventionClassNames(suffix, modelName).includes(declaration.name))
1135
994
  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(");
995
+ return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
1141
996
  }
1142
997
  function getConventionDescription(suffix, modelName) {
1143
998
  if (suffix === ".dictionary.ts")
1144
999
  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`;
1000
+ const names = [
1001
+ ...getConventionClassNames(suffix, modelName),
1002
+ ...suffix === ".constant.ts" ? ["enumOf classes"] : []
1003
+ ];
1004
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
1154
1005
  }
1155
1006
  var sysRootTypes = { apps: "app", libs: "lib" };
1156
1007
  function getSysRootEntry(file) {
@@ -1182,13 +1033,7 @@ function getModuleUiWarning(file) {
1182
1033
  const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
1183
1034
  if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
1184
1035
  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
- };
1036
+ return fileWarning("akan.layout.module-ui-file", "layout", file, `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`);
1192
1037
  }
1193
1038
  function getModuleInfo(file) {
1194
1039
  const segments = file.split("/");
@@ -1204,10 +1049,8 @@ function getModuleInfo(file) {
1204
1049
  if (segments.length !== libIndex + 3)
1205
1050
  return null;
1206
1051
  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
- }
1052
+ if (moduleName.startsWith("__"))
1053
+ return { moduleName, fileName, kind: "scalar" };
1211
1054
  if (moduleName.startsWith("_"))
1212
1055
  return { moduleName, fileName, kind: "service" };
1213
1056
  return { moduleName, fileName, kind: "database" };
@@ -1216,7 +1059,7 @@ function isExportedConst(declaration) {
1216
1059
  return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
1217
1060
  }
1218
1061
  function isExported(node) {
1219
- return !!ts3.getCombinedModifierFlags(node) && !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
1062
+ return !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
1220
1063
  }
1221
1064
  function isFunctionLikeInitializer(node) {
1222
1065
  return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
@@ -1262,7 +1105,7 @@ function toPascalCase(value) {
1262
1105
  return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
1263
1106
  }
1264
1107
  function toPosix(value) {
1265
- return value.split(path.sep).join("/");
1108
+ return value.split(path2.sep).join("/");
1266
1109
  }
1267
1110
  function groupBy(items, getKey) {
1268
1111
  const grouped = new Map;
@@ -1279,24 +1122,16 @@ function compareWarnings(a, b) {
1279
1122
  // pkgs/@akanjs/cli/quality/quality.script.ts
1280
1123
  import { Logger } from "akanjs/common";
1281
1124
 
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]) {
1125
+ class QualityScript extends script("quality") {
1291
1126
  async scan(workspace, format = "text") {
1292
1127
  const spinner = workspace.spinning("Scanning Akan code quality...");
1293
- const result = await this.qualityRunner.scan(workspace);
1128
+ const result = await new AkanQualityScanner().scan(workspace.workspaceRoot);
1294
1129
  spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
1295
1130
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
1296
1131
  }
1297
1132
  async ssr(workspace, format = "text") {
1298
1133
  const spinner = workspace.spinning("Measuring Akan server/client render balance...");
1299
- const scanned = await this.qualityRunner.scan(workspace);
1134
+ const scanned = await new AkanQualityScanner().scan(workspace.workspaceRoot);
1300
1135
  const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
1301
1136
  spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
1302
1137
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
@@ -1305,15 +1140,7 @@ class QualityScript extends script("quality", [QualityRunner]) {
1305
1140
 
1306
1141
  // pkgs/@akanjs/cli/quality/quality.command.ts
1307
1142
  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) {
1143
+ 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
1144
  if (action === "scan")
1318
1145
  await this.qualityScript.scan(workspace, format);
1319
1146
  else if (action === "ssr")