@akanjs/cli 3.0.0-alpha.4 → 3.0.0-alpha.41

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 (108) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-rk6twzvj.js → abstractCompactor-20e0cwbs.js} +3 -3
  5. package/{agent.command-jfz8yfwy.js → agent.command-e835fjem.js} +9 -8
  6. package/{application.command-mxyqtd6p.js → application.command-sjtbk6tr.js} +7 -6
  7. package/{applicationBuildRunner-cwk52sb7.js → applicationBuildRunner-7gnfz7ww.js} +7 -8
  8. package/{applicationReleasePackager-yrrhbkv3.js → applicationReleasePackager-93nzdfsy.js} +2 -2
  9. package/buildBatch.proc.js +6 -5
  10. package/{capacitorApp-j357ak1g.js → capacitorApp-qv7g3dp2.js} +29 -28
  11. package/{cloud.command-f87dwe52.js → cloud.command-y0e4eyqz.js} +11 -10
  12. package/{context.command-z5bf8bn7.js → context.command-vn87jb08.js} +17 -16
  13. package/{dependencyScanner-f6grrep3.js → dependencyScanner-rbs2407b.js} +1 -1
  14. package/getDirname-ka11hm7e.js +8 -0
  15. package/{guideline.command-1m1ctar3.js → guideline.command-s4hn2bsy.js} +7 -6
  16. package/guidelines/conventions/conventions.generate.json +25 -0
  17. package/guidelines/conventions/conventions.instruction.md +1193 -0
  18. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  19. package/guidelines/framework/framework.instruction.md +1 -0
  20. package/guidelines/ssrRule/ssrRule.generate.json +31 -0
  21. package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
  22. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  23. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +972 -0
  24. package/incrementalBuilder.proc.js +7 -6
  25. package/{index-1s71fwrq.js → index-0h212yqe.js} +13 -13
  26. package/index-1577bej2.js +7 -0
  27. package/{index-e3qskyt2.js → index-33zce8gt.js} +8 -4
  28. package/{index-csqk9z5j.js → index-3dp7x6rc.js} +3 -3
  29. package/{index-cn24dcqn.js → index-73rnbwgg.js} +15 -7
  30. package/{index-tq1y56tp.js → index-8d2sv6pw.js} +39 -16
  31. package/{index-jtm86byd.js → index-8vkkm7v0.js} +125 -66
  32. package/{index-r4w8cry5.js → index-9jsnb4vw.js} +4 -4
  33. package/{index-ybybkec0.js → index-becx1t0k.js} +6 -6
  34. package/{index-4yp06pn1.js → index-g9e2fhw2.js} +49 -32
  35. package/{index-cap2k2ch.js → index-hg0qvhmx.js} +152 -241
  36. package/{index-xwgm94zr.js → index-j37qq1f2.js} +1 -1
  37. package/{index-n0z3f2xg.js → index-k6fvbcj1.js} +37 -6
  38. package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
  39. package/{index-vhv3xnjz.js → index-mykmf6wd.js} +8 -8
  40. package/{index-y0k42k04.js → index-mz5jtaq7.js} +1 -1
  41. package/{index-jk9q6pse.js → index-n706txcv.js} +5 -3
  42. package/{index-hjzr8ja1.js → index-qc0tnhs2.js} +1 -1
  43. package/{index-rh16j3c7.js → index-qnv0aqmm.js} +12 -8
  44. package/{index-3pgtw35d.js → index-sh01r3pm.js} +2 -2
  45. package/{index-xj8bks3p.js → index-t71zvmt9.js} +9 -11
  46. package/{index-4zgs8g1w.js → index-tsmjxph2.js} +1 -1
  47. package/{index-fmgbwsr5.js → index-x33gnap3.js} +4 -4
  48. package/{index-b978vs2e.js → index-xyrb7pk5.js} +3 -3
  49. package/index.js +21 -20
  50. package/{library.command-pwfx8dx2.js → library.command-v5cyaw9t.js} +6 -5
  51. package/{localRegistry.command-r4zjx5py.js → localRegistry.command-53t758pt.js} +10 -9
  52. package/{module.command-0t3ewata.js → module.command-pq6w84xg.js} +10 -9
  53. package/{package.command-7hmkxvpa.js → package.command-qkcs60ts.js} +6 -5
  54. package/package.json +3 -4
  55. package/{page.command-st6r4s0q.js → page.command-qj5bfnh9.js} +6 -5
  56. package/{primitive.command-1qkxpzsn.js → primitive.command-bw0nes6x.js} +11 -10
  57. package/{quality.command-hy5vt1bh.js → quality.command-g1s2wt8b.js} +532 -87
  58. package/{recipeScanner-tghkzrrt.js → recipeScanner-zrzev25x.js} +3 -3
  59. package/{repair.command-nakkq2zp.js → repair.command-mzwtejpk.js} +8 -7
  60. package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-7fycdqyv.js} +3 -39
  61. package/{scalar.command-fjq272gs.js → scalar.command-rm6k3c65.js} +9 -8
  62. package/templates/app/env/env.server.debug.ts.template +1 -5
  63. package/templates/app/env/env.server.develop.ts.template +1 -5
  64. package/templates/app/env/env.server.local.ts.template +1 -5
  65. package/templates/app/env/env.server.main.ts.template +1 -5
  66. package/templates/app/page/styles.css.template +3 -3
  67. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  68. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  69. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  70. package/templates/crudPages/new/page.tsx +1 -1
  71. package/templates/crudSinglePage/page.tsx +1 -1
  72. package/templates/env/_env.server.type.ts +7 -5
  73. package/templates/lib/db.ts +4 -2
  74. package/templates/lib/dict.ts +1 -1
  75. package/templates/lib/sig.ts +1 -1
  76. package/templates/lib/srv.ts +6 -2
  77. package/templates/lib/useClient.ts +9 -3
  78. package/templates/module/__Model__.Util.tsx +1 -1
  79. package/templates/module/__Model__.Zone.tsx +3 -5
  80. package/templates/workspaceRoot/AGENTS.md.template +0 -831
  81. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  82. package/templates/workspaceRoot/biome.json.template +4 -217
  83. package/{typeChecker-kravn7ns.js → typeChecker-g6bh0xfv.js} +1 -1
  84. package/typecheck.proc.js +1 -1
  85. package/{workflow.command-jswbgk90.js → workflow.command-g2cgcgxn.js} +16 -15
  86. package/{workspace.command-h9wwcjpt.js → workspace.command-f7jerqb2.js} +70 -52
  87. package/templates/libRoot/base/baseLogic.ts +0 -14
  88. package/templates/libRoot/base/index.ts +0 -8
  89. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  90. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -33
  91. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  92. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  93. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -17
  94. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  95. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  96. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  97. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  98. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
  99. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
  100. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  101. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  102. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  103. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
  104. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  105. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  106. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  107. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  108. /package/{index-r24hmh0q.js → index-yb2wsc7f.js} +0 -0
@@ -7,18 +7,454 @@ import {
7
7
  command,
8
8
  runner,
9
9
  script
10
- } from "./index-jk9q6pse.js";
11
- import"./index-jtm86byd.js";
12
- import"./index-6pz1j0zj.js";
10
+ } from "./index-n706txcv.js";
11
+ import {
12
+ appRootAllowedFiles,
13
+ libFacetRootAllowedFiles
14
+ } from "./index-8vkkm7v0.js";
15
+ import"./index-mxvakhsm.js";
16
+ import"./index-1577bej2.js";
13
17
  import"./index-67546d0j.js";
14
- import"./index-r24hmh0q.js";
18
+ import"./index-yb2wsc7f.js";
15
19
 
16
20
  // pkgs/@akanjs/devkit/qualityScanner.ts
17
21
  import { createHash } from "crypto";
18
22
  import { readdir, readFile, stat } from "fs/promises";
19
23
  import path from "path";
24
+ import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
20
25
  import ignore from "ignore";
26
+ import ts3 from "typescript";
27
+
28
+ // pkgs/@akanjs/devkit/formSetterScanner.ts
21
29
  import ts from "typescript";
30
+
31
+ class FormSetterScanner {
32
+ static #fieldSetter = /^set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*$/;
33
+ scan(sourceFiles) {
34
+ return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".tsx")).flatMap((f) => this.#scanFile(f));
35
+ }
36
+ #scanFile({ file, sourceFile }) {
37
+ const wrapped = [];
38
+ const visit = (node) => {
39
+ if (ts.isJsxAttribute(node)) {
40
+ const setter = FormSetterScanner.#wrappedSetterOf(node);
41
+ if (setter)
42
+ wrapped.push({ setter, line: FormSetterScanner.#lineOf(sourceFile, node) });
43
+ }
44
+ ts.forEachChild(node, visit);
45
+ };
46
+ visit(sourceFile);
47
+ if (!wrapped.length)
48
+ return [];
49
+ const setters = [...new Set(wrapped.map((entry) => entry.setter))].sort();
50
+ return [
51
+ {
52
+ rule: "akan.agent.unpublished-form-setter",
53
+ scope: "agent",
54
+ severity: "warning",
55
+ message: setters.length === 1 ? `${setters[0]} is reached through a wrapper, so that field publishes no agent tool and carries no data-akan-action.` : `${setters.length} field setters are reached through a wrapper, so those fields publish no agent tool and carry no data-akan-action: ${setters.join(", ")}.`,
56
+ file,
57
+ line: wrapped[0]?.line,
58
+ locations: wrapped.map(({ line }) => ({ file, line }))
59
+ }
60
+ ];
61
+ }
62
+ static #wrappedSetterOf(attribute) {
63
+ if (!ts.isIdentifier(attribute.name) || !/^on[A-Z]/.test(attribute.name.text))
64
+ return null;
65
+ const initializer = attribute.initializer;
66
+ if (!initializer || !ts.isJsxExpression(initializer) || !initializer.expression)
67
+ return null;
68
+ const handler = initializer.expression;
69
+ if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler))
70
+ return null;
71
+ if (!handler.parameters.length)
72
+ return null;
73
+ return FormSetterScanner.#setterCallIn(handler.body);
74
+ }
75
+ static #setterCallIn(node) {
76
+ let found = null;
77
+ const visit = (current) => {
78
+ if (found)
79
+ return;
80
+ if (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
81
+ const { expression, name } = current.expression;
82
+ if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === "st" && expression.name.text === "do" && FormSetterScanner.#fieldSetter.test(name.text)) {
83
+ found = name.text;
84
+ return;
85
+ }
86
+ }
87
+ ts.forEachChild(current, visit);
88
+ };
89
+ visit(node);
90
+ return found;
91
+ }
92
+ static #lineOf(sourceFile, node) {
93
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
94
+ }
95
+ }
96
+
97
+ // pkgs/@akanjs/devkit/ssrScanner.ts
98
+ import ts2 from "typescript";
99
+ var STATIC_COMPONENT_MIN_MASS = 4;
100
+ var MIXED_COMPONENT_MIN_MASS = 10;
101
+ var MIXED_COMPONENT_MAX_TOUCHES = 2;
102
+ var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
103
+
104
+ class SsrScanner {
105
+ static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
106
+ static #clientGlobals = new Set([
107
+ "window",
108
+ "document",
109
+ "navigator",
110
+ "localStorage",
111
+ "sessionStorage",
112
+ "location",
113
+ "history",
114
+ "screen",
115
+ "matchMedia",
116
+ "IntersectionObserver",
117
+ "ResizeObserver",
118
+ "MutationObserver",
119
+ "requestAnimationFrame",
120
+ "WebSocket"
121
+ ]);
122
+ static #clientRuntimeImports = new Set(["st", "fetch"]);
123
+ scan(sourceFiles) {
124
+ const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
125
+ return {
126
+ warnings: [
127
+ ...componentFiles.flatMap((sourceFile) => this.#scanFile(sourceFile)),
128
+ ...this.#scanModules(componentFiles)
129
+ ],
130
+ balance: this.#measureBalance(componentFiles)
131
+ };
132
+ }
133
+ #scanFile(sourceFile) {
134
+ if (!this.#hasUseClient(sourceFile.sourceFile))
135
+ return [];
136
+ const vendorNames = this.#getVendorNames(sourceFile.sourceFile);
137
+ const hasVendorImport = this.#hasVendorImport(sourceFile.sourceFile);
138
+ const importsClientRuntime = this.#importsClientRuntime(sourceFile.sourceFile);
139
+ const components = this.#getComponents(sourceFile, vendorNames);
140
+ const warnings = [];
141
+ if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
142
+ warnings.push({
143
+ rule: "akan.ssr.unnecessary-use-client",
144
+ scope: "ssr",
145
+ severity: "warning",
146
+ file: sourceFile.file,
147
+ line: 1,
148
+ message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
149
+ });
150
+ }
151
+ for (const component of components) {
152
+ if (component.vendorTags)
153
+ continue;
154
+ if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
155
+ warnings.push({
156
+ rule: "akan.ssr.client-static-component",
157
+ scope: "ssr",
158
+ severity: "warning",
159
+ file: sourceFile.file,
160
+ line: component.line,
161
+ 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.`
162
+ });
163
+ continue;
164
+ }
165
+ if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
166
+ warnings.push({
167
+ rule: "akan.ssr.client-static-markup",
168
+ scope: "ssr",
169
+ severity: "warning",
170
+ file: sourceFile.file,
171
+ line: component.line,
172
+ 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.`
173
+ });
174
+ }
175
+ }
176
+ warnings.push(...this.#getMountLoadWarnings(sourceFile));
177
+ warnings.push(...this.#getTemplateStateWarnings(sourceFile));
178
+ return warnings;
179
+ }
180
+ #scanModules(sourceFiles) {
181
+ const modules = new Map;
182
+ for (const sourceFile of sourceFiles) {
183
+ const moduleDir = this.#getModuleDir(sourceFile.file);
184
+ if (!moduleDir)
185
+ continue;
186
+ const entry = modules.get(moduleDir) ?? { clientMass: 0, serverFiles: 0, line: sourceFile.file };
187
+ if (this.#hasUseClient(sourceFile.sourceFile))
188
+ entry.clientMass += this.#getMass(sourceFile.sourceFile);
189
+ else if (/\.(Unit|View)\.tsx$/.test(sourceFile.file))
190
+ entry.serverFiles += 1;
191
+ modules.set(moduleDir, entry);
192
+ }
193
+ return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
194
+ rule: "akan.ssr.module-missing-server-view",
195
+ scope: "ssr",
196
+ severity: "warning",
197
+ file: entry.line,
198
+ message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
199
+ }));
200
+ }
201
+ #getMountLoadWarnings(sourceFile) {
202
+ const warnings = [];
203
+ const visit = (node) => {
204
+ if (this.#isMountEffect(sourceFile.sourceFile, node)) {
205
+ for (const load of this.#getLoadCalls(sourceFile.sourceFile, node)) {
206
+ warnings.push({
207
+ rule: "akan.ssr.client-mount-load",
208
+ scope: "ssr",
209
+ severity: "warning",
210
+ file: sourceFile.file,
211
+ line: this.#getLine(sourceFile.sourceFile, load.node),
212
+ message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
213
+ });
214
+ }
215
+ }
216
+ ts2.forEachChild(node, visit);
217
+ };
218
+ ts2.forEachChild(sourceFile.sourceFile, visit);
219
+ return warnings;
220
+ }
221
+ #isMountEffect(sourceFile, node) {
222
+ if (!ts2.isCallExpression(node))
223
+ return false;
224
+ const callee = node.expression.getText(sourceFile);
225
+ if (callee !== "useEffect" && callee !== "useLayoutEffect")
226
+ return false;
227
+ const deps = node.arguments[1];
228
+ return !!deps && ts2.isArrayLiteralExpression(deps) && deps.elements.length === 0;
229
+ }
230
+ #getLoadCalls(sourceFile, node) {
231
+ const calls = [];
232
+ const visit = (child) => {
233
+ if (ts2.isCallExpression(child)) {
234
+ const callee = child.expression.getText(sourceFile);
235
+ if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
236
+ calls.push({ callee, node: child });
237
+ }
238
+ ts2.forEachChild(child, visit);
239
+ };
240
+ ts2.forEachChild(node, visit);
241
+ return calls;
242
+ }
243
+ #getTemplateStateWarnings(sourceFile) {
244
+ if (!sourceFile.file.endsWith(".Template.tsx"))
245
+ return [];
246
+ const warnings = [];
247
+ const visit = (node) => {
248
+ if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
249
+ warnings.push({
250
+ rule: "akan.ssr.template-client-state",
251
+ scope: "ssr",
252
+ severity: "warning",
253
+ file: sourceFile.file,
254
+ line: this.#getLine(sourceFile.sourceFile, node),
255
+ message: "Template holds form state in useState. Templates are store-driven and carry no local state."
256
+ });
257
+ }
258
+ ts2.forEachChild(node, visit);
259
+ };
260
+ ts2.forEachChild(sourceFile.sourceFile, visit);
261
+ return warnings;
262
+ }
263
+ #measureBalance(sourceFiles) {
264
+ const scopes = new Map;
265
+ for (const sourceFile of sourceFiles) {
266
+ const segments = sourceFile.file.split("/");
267
+ const scope = `${segments[0]}/${segments[1]}`;
268
+ const entry = scopes.get(scope) ?? { serverMass: 0, clientMass: 0 };
269
+ const mass = this.#getMass(sourceFile.sourceFile);
270
+ if (this.#hasUseClient(sourceFile.sourceFile))
271
+ entry.clientMass += mass;
272
+ else
273
+ entry.serverMass += mass;
274
+ scopes.set(scope, entry);
275
+ }
276
+ const entries = [...scopes].map(([scope, mass]) => ({ scope, ...mass, serverShare: getShare(mass.serverMass, mass.clientMass) })).sort((a, b) => a.scope.localeCompare(b.scope));
277
+ if (entries.length < 2)
278
+ return entries;
279
+ const serverMass = entries.reduce((sum, entry) => sum + entry.serverMass, 0);
280
+ const clientMass = entries.reduce((sum, entry) => sum + entry.clientMass, 0);
281
+ return [...entries, { scope: "workspace", serverMass, clientMass, serverShare: getShare(serverMass, clientMass) }];
282
+ }
283
+ #getComponents(sourceFile, vendorNames) {
284
+ const components = [];
285
+ for (const statement of sourceFile.sourceFile.statements) {
286
+ for (const { name, node } of this.#getComponentNodes(statement)) {
287
+ if (!/^[A-Z]/.test(name))
288
+ continue;
289
+ components.push({
290
+ name,
291
+ line: this.#getLine(sourceFile.sourceFile, node),
292
+ mass: this.#getMass(node),
293
+ touches: this.#getTouches(sourceFile.sourceFile, node),
294
+ vendorTags: [...this.#getTagNames(sourceFile.sourceFile, node)].some((tag) => vendorNames.has(tag))
295
+ });
296
+ }
297
+ }
298
+ return components;
299
+ }
300
+ #getComponentNodes(statement) {
301
+ if (ts2.isFunctionDeclaration(statement) && statement.body)
302
+ return [{ name: statement.name?.text ?? "default", node: statement.body }];
303
+ if (!ts2.isVariableStatement(statement))
304
+ return [];
305
+ return statement.declarationList.declarations.flatMap((declaration) => {
306
+ if (!ts2.isIdentifier(declaration.name) || !declaration.initializer)
307
+ return [];
308
+ if (!ts2.isArrowFunction(declaration.initializer) && !ts2.isFunctionExpression(declaration.initializer))
309
+ return [];
310
+ return [{ name: declaration.name.text, node: declaration.initializer }];
311
+ });
312
+ }
313
+ #getTouches(sourceFile, node) {
314
+ const touches = [];
315
+ const visit = (child) => {
316
+ if (ts2.isCallExpression(child)) {
317
+ const callee = child.expression.getText(sourceFile);
318
+ const bareName = callee.split(".").pop() ?? callee;
319
+ if (callee === "createContext" || callee === "lazy")
320
+ touches.push(callee);
321
+ else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
322
+ touches.push(bareName);
323
+ }
324
+ if (ts2.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
325
+ touches.push(child.name.getText(sourceFile));
326
+ if (ts2.isPropertyAccessExpression(child)) {
327
+ const root = getAccessRoot(child);
328
+ if (root === "st")
329
+ touches.push("st");
330
+ else if (SsrScanner.#clientGlobals.has(root))
331
+ touches.push(root);
332
+ }
333
+ ts2.forEachChild(child, visit);
334
+ };
335
+ ts2.forEachChild(node, visit);
336
+ return touches;
337
+ }
338
+ #getTagNames(sourceFile, node) {
339
+ const tags = new Set;
340
+ const visit = (child) => {
341
+ if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
342
+ tags.add(child.tagName.getText(sourceFile).split(".")[0]);
343
+ ts2.forEachChild(child, visit);
344
+ };
345
+ ts2.forEachChild(node, visit);
346
+ return tags;
347
+ }
348
+ #getMass(node) {
349
+ let mass = 0;
350
+ const visit = (child) => {
351
+ if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
352
+ mass += 1;
353
+ ts2.forEachChild(child, visit);
354
+ };
355
+ ts2.forEachChild(node, visit);
356
+ return mass;
357
+ }
358
+ #hasUseClient(sourceFile) {
359
+ const first = sourceFile.statements[0];
360
+ if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
361
+ return false;
362
+ return first.expression.text === "use client";
363
+ }
364
+ #hasVendorImport(sourceFile) {
365
+ return sourceFile.statements.some((statement) => ts2.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
366
+ }
367
+ #getVendorNames(sourceFile) {
368
+ const names = new Set;
369
+ for (const statement of sourceFile.statements) {
370
+ if (!ts2.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
371
+ continue;
372
+ const clause = statement.importClause;
373
+ if (clause?.name)
374
+ names.add(clause.name.text);
375
+ if (clause?.namedBindings && ts2.isNamespaceImport(clause.namedBindings))
376
+ names.add(clause.namedBindings.name.text);
377
+ if (clause?.namedBindings && ts2.isNamedImports(clause.namedBindings))
378
+ for (const element of clause.namedBindings.elements)
379
+ names.add(element.name.text);
380
+ }
381
+ return names;
382
+ }
383
+ #importsClientRuntime(sourceFile) {
384
+ for (const statement of sourceFile.statements) {
385
+ if (!ts2.isImportDeclaration(statement))
386
+ continue;
387
+ const bindings = statement.importClause?.namedBindings;
388
+ if (!bindings || !ts2.isNamedImports(bindings))
389
+ continue;
390
+ if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
391
+ return true;
392
+ }
393
+ return false;
394
+ }
395
+ #isConventionClientFile(file) {
396
+ if (file.endsWith("/index_.tsx"))
397
+ return true;
398
+ return /\.(Zone|Template|Util)\.tsx$/.test(file) && this.#getModuleDir(file) !== null;
399
+ }
400
+ #isBalancedFile(file) {
401
+ if (!file.endsWith(".tsx") || file.endsWith(".test.tsx") || file.endsWith(".spec.tsx"))
402
+ return false;
403
+ const segments = file.split("/");
404
+ if (segments[0] !== "apps" && segments[0] !== "libs")
405
+ return false;
406
+ return segments[2] === "ui" || segments[2] === "lib";
407
+ }
408
+ #getModuleDir(file) {
409
+ const segments = file.split("/");
410
+ const libIndex = segments.indexOf("lib");
411
+ if (libIndex < 1 || segments.length <= libIndex + 2)
412
+ return null;
413
+ const moduleName = segments[libIndex + 1];
414
+ if (moduleName.startsWith("_"))
415
+ return null;
416
+ return segments.slice(0, libIndex + 2).join("/");
417
+ }
418
+ #getLine(sourceFile, node) {
419
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
420
+ }
421
+ }
422
+ var SSR_SERVER_SHARE_TARGET = 0.5;
423
+ function formatSsrBalance(balance) {
424
+ if (balance.length === 0)
425
+ return ["No component files found."];
426
+ return balance.map((entry) => {
427
+ const total = entry.serverMass + entry.clientMass;
428
+ const share = `${Math.round(entry.serverShare * 100)}% server`;
429
+ const counts = `${entry.serverMass} of ${total} JSX elements, ${entry.clientMass} client`;
430
+ const flag = entry.serverShare < SSR_SERVER_SHARE_TARGET ? ` <- below the ${Math.round(SSR_SERVER_SHARE_TARGET * 100)}% target` : "";
431
+ return ` ${entry.scope}: ${share} (${counts})${flag}`;
432
+ });
433
+ }
434
+ function getShare(serverMass, clientMass) {
435
+ const total = serverMass + clientMass;
436
+ return total === 0 ? 1 : serverMass / total;
437
+ }
438
+ function getSpecifier(statement) {
439
+ return ts2.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
440
+ }
441
+ function isVendorSpecifier(specifier) {
442
+ if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
443
+ return false;
444
+ if (specifier === "react" || specifier === "react-dom" || specifier.startsWith("react/"))
445
+ return false;
446
+ if (specifier.startsWith("node:"))
447
+ return false;
448
+ return !/^(akanjs|@akanjs|@libs|@apps|@contract)(\/|$)/.test(specifier);
449
+ }
450
+ function getAccessRoot(node) {
451
+ let current = node;
452
+ while (ts2.isPropertyAccessExpression(current))
453
+ current = current.expression;
454
+ return ts2.isIdentifier(current) ? current.text : "";
455
+ }
456
+
457
+ // pkgs/@akanjs/devkit/qualityScanner.ts
22
458
  var MAX_FILE_LINES = 2000;
23
459
  var PLACEHOLDER_EXPORT_NAMES = new Set([
24
460
  "aa",
@@ -39,30 +475,10 @@ var SUGGESTED_RULES = [
39
475
  "Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
40
476
  "Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
41
477
  "Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
478
+ "Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a _postSet<Field> hook on the store.",
42
479
  "Move shared app utilities to apps/*/common instead of creating apps/*/base.",
43
480
  "Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
44
481
  ];
45
- var APP_ROOT_FILES = new Set([
46
- "akan.app.json",
47
- "akan.config.ts",
48
- "capacitor.config.ts",
49
- "client.ts",
50
- "main.ts",
51
- "package.json",
52
- "server.ts",
53
- "tsconfig.json"
54
- ]);
55
- var LIB_ROOT_FILES = new Set([
56
- "cnst.ts",
57
- "db.ts",
58
- "dict.ts",
59
- "option.ts",
60
- "sig.ts",
61
- "srv.ts",
62
- "st.ts",
63
- "useClient.ts",
64
- "useServer.ts"
65
- ]);
66
482
  var CONVENTION_SUFFIXES = [
67
483
  ".constant.ts",
68
484
  ".dictionary.ts",
@@ -71,20 +487,6 @@ var CONVENTION_SUFFIXES = [
71
487
  ".signal.ts",
72
488
  ".store.ts"
73
489
  ];
74
- var PAGE_RESERVED_EXPORTS = new Set([
75
- "pageConfig",
76
- "head",
77
- "metadata",
78
- "generateHead",
79
- "generateMetadata",
80
- "fonts",
81
- "manifest",
82
- "theme",
83
- "reconnect",
84
- "wsConnect",
85
- "layoutStyle",
86
- "gaTrackingId"
87
- ]);
88
490
  var RULE_FIXES = {
89
491
  "akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
90
492
  "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
@@ -97,11 +499,18 @@ var RULE_FIXES = {
97
499
  "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
98
500
  "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
99
501
  "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
502
+ "akan.agent.unpublished-form-setter": "Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a _postSet<Field> hook on the store, keeping the generated setter on the control.",
100
503
  "akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
101
504
  "akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
102
505
  "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
103
506
  "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
104
- "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component."
507
+ "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
508
+ "akan.ssr.unnecessary-use-client": 'Delete the "use client" directive so the file renders on the server. If it exists only to wrap one client child, drop the wrapper and use the child directly.',
509
+ "akan.ssr.client-static-component": "Move the component to a server file \u2014 a <Model>.Unit.tsx / <Model>.View.tsx for a module, or a ui/ file with no directive \u2014 and reference it from the client file.",
510
+ "akan.ssr.client-static-markup": "Keep the interactive element in the client component and hoist the static subtree into a server component, then accept it as `children` or render it through a Unit/View reference.",
511
+ "akan.ssr.client-mount-load": "Load the data in the route with `fetch.initX(...)` / `fetch.viewX(...)` and pass the init/view object down as a prop; the client store hydrates from it and the effect goes away.",
512
+ "akan.ssr.module-missing-server-view": "Add a <Model>.Unit.tsx for list/card rendering and a <Model>.View.tsx for the detail surface, then have the Zone delegate to them.",
513
+ "akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
105
514
  };
106
515
  function getRuleFix(rule) {
107
516
  if (rule.startsWith("akan.convention"))
@@ -114,18 +523,22 @@ class AkanQualityScanner {
114
523
  const targetFiles = await this.#collectTargetFiles(workspaceRoot);
115
524
  const sourceFiles = await Promise.all(targetFiles.filter((file) => !AbstractDoc.isAbstractPath(file)).map((file) => this.#readSourceFile(workspaceRoot, file)));
116
525
  const abstractFiles = await Promise.all(targetFiles.filter((file) => AbstractDoc.isAbstractPath(file)).map((file) => this.#readTextFile(workspaceRoot, file)));
526
+ const ssr = new SsrScanner().scan(sourceFiles);
117
527
  const warnings = [
118
528
  ...this.#scanGlobalQuality(sourceFiles),
119
529
  ...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
120
530
  ...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
121
531
  ...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
122
532
  ...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
123
- ...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile))
533
+ ...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
534
+ ...new FormSetterScanner().scan(sourceFiles),
535
+ ...ssr.warnings
124
536
  ];
125
537
  return {
126
538
  workspaceRoot,
127
539
  scannedFiles: sourceFiles.length + abstractFiles.length,
128
540
  warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
541
+ ssrBalance: ssr.balance,
129
542
  suggestedRules: SUGGESTED_RULES
130
543
  };
131
544
  }
@@ -171,7 +584,7 @@ class AkanQualityScanner {
171
584
  file,
172
585
  absolutePath,
173
586
  content,
174
- sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, getScriptKind(file))
587
+ sourceFile: ts3.createSourceFile(file, content, ts3.ScriptTarget.Latest, true, getScriptKind(file))
175
588
  };
176
589
  }
177
590
  async#readTextFile(workspaceRoot, file) {
@@ -332,7 +745,7 @@ class AkanQualityScanner {
332
745
  #scanLayoutQuality(sourceFile) {
333
746
  const segments = sourceFile.file.split("/");
334
747
  const warnings = [];
335
- if (segments[0] === "apps" && segments.length === 3 && !APP_ROOT_FILES.has(segments[2])) {
748
+ if (segments[0] === "apps" && segments.length === 3 && !appRootAllowedFiles.has(segments[2])) {
336
749
  warnings.push({
337
750
  rule: "akan.layout.app-root-file",
338
751
  scope: "layout",
@@ -342,7 +755,7 @@ class AkanQualityScanner {
342
755
  });
343
756
  }
344
757
  const libRootFile = getLibRootFile(sourceFile.file);
345
- if (libRootFile && !LIB_ROOT_FILES.has(libRootFile)) {
758
+ if (libRootFile && !libFacetRootAllowedFiles.has(libRootFile)) {
346
759
  warnings.push({
347
760
  rule: "akan.layout.lib-root-file",
348
761
  scope: "layout",
@@ -368,6 +781,10 @@ function formatQualityScanResult(result) {
368
781
  "",
369
782
  ...formatQualityWarnings(result.warnings),
370
783
  "",
784
+ "SSR balance (component files, JSX elements rendered per side):",
785
+ "",
786
+ ...formatSsrBalance(result.ssrBalance),
787
+ "",
371
788
  "Suggested quality rules:",
372
789
  "",
373
790
  ...result.suggestedRules.map((rule) => ` - ${rule}`)
@@ -375,6 +792,24 @@ function formatQualityScanResult(result) {
375
792
  return sections.join(`
376
793
  `);
377
794
  }
795
+ function formatSsrScanResult(result) {
796
+ const sections = [
797
+ "Akan SSR Balance Scan",
798
+ `workspace: ${result.workspaceRoot}`,
799
+ `scanned files: ${result.scannedFiles}`,
800
+ `ssr warnings: ${result.warnings.length}`,
801
+ "",
802
+ "Server render share (component files, JSX elements rendered per side):",
803
+ "",
804
+ ...formatSsrBalance(result.ssrBalance),
805
+ "",
806
+ "Warnings:",
807
+ "",
808
+ ...formatQualityWarnings(result.warnings)
809
+ ];
810
+ return sections.join(`
811
+ `);
812
+ }
378
813
  function formatQualityWarnings(warnings) {
379
814
  if (warnings.length === 0)
380
815
  return ["No warnings found."];
@@ -396,7 +831,7 @@ function getExportedFunctionLikes(sourceFile) {
396
831
  const declarations = [];
397
832
  const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
398
833
  for (const statement of sourceFile.sourceFile.statements) {
399
- if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
834
+ if (ts3.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
400
835
  declarations.push({
401
836
  name: statement.name.text,
402
837
  kind: "function",
@@ -406,7 +841,7 @@ function getExportedFunctionLikes(sourceFile) {
406
841
  duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
407
842
  });
408
843
  }
409
- if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
844
+ if (ts3.isClassDeclaration(statement) && statement.name && isExported(statement)) {
410
845
  declarations.push({
411
846
  name: statement.name.text,
412
847
  kind: "class",
@@ -416,9 +851,9 @@ function getExportedFunctionLikes(sourceFile) {
416
851
  duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
417
852
  });
418
853
  }
419
- if (ts.isVariableStatement(statement) && isExported(statement)) {
854
+ if (ts3.isVariableStatement(statement) && isExported(statement)) {
420
855
  for (const declaration of statement.declarationList.declarations) {
421
- if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
856
+ if (!ts3.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
422
857
  continue;
423
858
  declarations.push({
424
859
  name: declaration.name.text,
@@ -457,14 +892,14 @@ function isInLibModule(file) {
457
892
  return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
458
893
  }
459
894
  function isEnumClassStatement(sourceFile, statement) {
460
- if (!ts.isClassDeclaration(statement))
895
+ if (!ts3.isClassDeclaration(statement))
461
896
  return false;
462
- const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
897
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
463
898
  const expression = heritageClause?.types[0]?.expression;
464
899
  return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
465
900
  }
466
901
  function getExportedClassNames(sourceFile) {
467
- return sourceFile.statements.filter((statement) => ts.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
902
+ return sourceFile.statements.filter((statement) => ts3.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
468
903
  }
469
904
  function isComponentDeclarationFile(file) {
470
905
  if (!file.endsWith(".tsx"))
@@ -480,7 +915,7 @@ function isComponentDeclarationFile(file) {
480
915
  function getComponentFileDeclarations(sourceFile) {
481
916
  const reExportedNames = new Set;
482
917
  for (const statement of sourceFile.statements) {
483
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
918
+ if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
484
919
  for (const element of statement.exportClause.elements)
485
920
  reExportedNames.add((element.propertyName ?? element.name).text);
486
921
  }
@@ -490,19 +925,19 @@ function getComponentFileDeclarations(sourceFile) {
490
925
  const isDefaultExport = isDefaultExportStatement(statement);
491
926
  const inlineExported = isExported(statement);
492
927
  const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
493
- if (ts.isInterfaceDeclaration(statement))
928
+ if (ts3.isInterfaceDeclaration(statement))
494
929
  add(statement.name.text, "interface", getLine(sourceFile, statement));
495
- else if (ts.isTypeAliasDeclaration(statement))
930
+ else if (ts3.isTypeAliasDeclaration(statement))
496
931
  add(statement.name.text, "type", getLine(sourceFile, statement));
497
- else if (ts.isEnumDeclaration(statement))
932
+ else if (ts3.isEnumDeclaration(statement))
498
933
  add(statement.name.text, "enum", getLine(sourceFile, statement));
499
- else if (ts.isFunctionDeclaration(statement) && statement.name)
934
+ else if (ts3.isFunctionDeclaration(statement) && statement.name)
500
935
  add(statement.name.text, "function", getLine(sourceFile, statement));
501
- else if (ts.isClassDeclaration(statement) && statement.name)
936
+ else if (ts3.isClassDeclaration(statement) && statement.name)
502
937
  add(statement.name.text, "class", getLine(sourceFile, statement));
503
- else if (ts.isVariableStatement(statement)) {
938
+ else if (ts3.isVariableStatement(statement)) {
504
939
  for (const declaration of statement.declarationList.declarations) {
505
- if (!ts.isIdentifier(declaration.name))
940
+ if (!ts3.isIdentifier(declaration.name))
506
941
  continue;
507
942
  const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
508
943
  add(declaration.name.text, kind, getLine(sourceFile, declaration));
@@ -514,15 +949,15 @@ function getComponentFileDeclarations(sourceFile) {
514
949
  function getCompoundComponentNames(sourceFile) {
515
950
  const names = new Set;
516
951
  for (const statement of sourceFile.statements) {
517
- if (!ts.isExpressionStatement(statement))
952
+ if (!ts3.isExpressionStatement(statement))
518
953
  continue;
519
954
  const { expression } = statement;
520
- if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
955
+ if (!ts3.isBinaryExpression(expression) || expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken)
521
956
  continue;
522
- if (!ts.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
957
+ if (!ts3.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
523
958
  continue;
524
959
  names.add(expression.left.name.text);
525
- if (ts.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
960
+ if (ts3.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
526
961
  names.add(expression.right.text);
527
962
  }
528
963
  return names;
@@ -536,15 +971,15 @@ function isRestrictedInternalKind(kind) {
536
971
  function isAllowedComponentExport(declaration, isPage) {
537
972
  if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
538
973
  return true;
539
- return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
974
+ return isPage && RESERVED_ROUTE_CONFIG_EXPORTS.has(declaration.name);
540
975
  }
541
976
  function isPascalCaseName(name) {
542
977
  return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
543
978
  }
544
979
  function isDefaultExportStatement(statement) {
545
- if (ts.isExportAssignment(statement))
980
+ if (ts3.isExportAssignment(statement))
546
981
  return !statement.isExportEquals;
547
- return !!(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Default);
982
+ return !!(ts3.getCombinedModifierFlags(statement) & ts3.ModifierFlags.Default);
548
983
  }
549
984
  function getPlaceholderExportWarnings(sourceFile) {
550
985
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
@@ -574,7 +1009,7 @@ function getDictionaryTextWarnings(sourceFile) {
574
1009
  function getGlobalMutationWarnings(sourceFile) {
575
1010
  const warnings = [];
576
1011
  for (const statement of sourceFile.sourceFile.statements) {
577
- if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
1012
+ if (ts3.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
578
1013
  warnings.push({
579
1014
  rule: "akan.file.global-declaration",
580
1015
  scope: "file",
@@ -584,7 +1019,7 @@ function getGlobalMutationWarnings(sourceFile) {
584
1019
  message: "Global declarations require an explicit low-level integration allowlist."
585
1020
  });
586
1021
  }
587
- if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
1022
+ if (ts3.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
588
1023
  warnings.push({
589
1024
  rule: "akan.file.window-augmentation",
590
1025
  scope: "file",
@@ -594,7 +1029,7 @@ function getGlobalMutationWarnings(sourceFile) {
594
1029
  message: "Window augmentation should be isolated to approved browser integration files."
595
1030
  });
596
1031
  }
597
- if (ts.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
1032
+ if (ts3.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
598
1033
  warnings.push({
599
1034
  rule: "akan.file.prototype-mutation",
600
1035
  scope: "file",
@@ -612,23 +1047,23 @@ function getTopLevelDeclarations(sourceFile) {
612
1047
  }
613
1048
  function getTopLevelDeclaration(sourceFile, statement) {
614
1049
  const line = getLine(sourceFile, statement);
615
- if (ts.isClassDeclaration(statement) && statement.name) {
1050
+ if (ts3.isClassDeclaration(statement) && statement.name) {
616
1051
  return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
617
1052
  }
618
- if (ts.isFunctionDeclaration(statement) && statement.name) {
1053
+ if (ts3.isFunctionDeclaration(statement) && statement.name) {
619
1054
  return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
620
1055
  }
621
- if (ts.isInterfaceDeclaration(statement)) {
1056
+ if (ts3.isInterfaceDeclaration(statement)) {
622
1057
  return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
623
1058
  }
624
- if (ts.isTypeAliasDeclaration(statement)) {
1059
+ if (ts3.isTypeAliasDeclaration(statement)) {
625
1060
  return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
626
1061
  }
627
- if (ts.isEnumDeclaration(statement)) {
1062
+ if (ts3.isEnumDeclaration(statement)) {
628
1063
  return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
629
1064
  }
630
- if (ts.isVariableStatement(statement)) {
631
- return statement.declarationList.declarations.filter((declaration) => ts.isIdentifier(declaration.name)).map((declaration) => ({
1065
+ if (ts3.isVariableStatement(statement)) {
1066
+ return statement.declarationList.declarations.filter((declaration) => ts3.isIdentifier(declaration.name)).map((declaration) => ({
632
1067
  name: declaration.name.text,
633
1068
  kind: "variable",
634
1069
  line: getLine(sourceFile, declaration),
@@ -636,7 +1071,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
636
1071
  node: statement
637
1072
  }));
638
1073
  }
639
- if (ts.isExportDeclaration(statement)) {
1074
+ if (ts3.isExportDeclaration(statement)) {
640
1075
  return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
641
1076
  }
642
1077
  return [];
@@ -661,9 +1096,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
661
1096
  return false;
662
1097
  if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
663
1098
  return true;
664
- if (!ts.isClassDeclaration(declaration.node))
1099
+ if (!ts3.isClassDeclaration(declaration.node))
665
1100
  return false;
666
- const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
1101
+ const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts3.SyntaxKind.ExtendsKeyword);
667
1102
  const expression = heritageClause?.types[0]?.expression;
668
1103
  return !!expression && expression.getText().startsWith("enumOf(");
669
1104
  }
@@ -729,13 +1164,13 @@ function getModuleInfo(file) {
729
1164
  return { moduleName, fileName, kind: "database" };
730
1165
  }
731
1166
  function isExportedConst(declaration) {
732
- return declaration.exported && ts.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts.NodeFlags.Const) !== 0;
1167
+ return declaration.exported && ts3.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts3.NodeFlags.Const) !== 0;
733
1168
  }
734
1169
  function isExported(node) {
735
- return !!ts.getCombinedModifierFlags(node) && !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
1170
+ return !!ts3.getCombinedModifierFlags(node) && !!(ts3.getCombinedModifierFlags(node) & ts3.ModifierFlags.Export);
736
1171
  }
737
1172
  function isFunctionLikeInitializer(node) {
738
- return !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
1173
+ return !!node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node));
739
1174
  }
740
1175
  function getBodyFingerprint(sourceFile, node) {
741
1176
  if (!node)
@@ -757,7 +1192,7 @@ async function isDirectory(absolutePath) {
757
1192
  }
758
1193
  }
759
1194
  function getScriptKind(file) {
760
- return file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
1195
+ return file.endsWith(".tsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
761
1196
  }
762
1197
  function getLine(sourceFile, node) {
763
1198
  return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
@@ -810,6 +1245,13 @@ class QualityScript extends script("quality", [QualityRunner]) {
810
1245
  spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
811
1246
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
812
1247
  }
1248
+ async ssr(workspace, format = "text") {
1249
+ const spinner = workspace.spinning("Measuring Akan server/client render balance...");
1250
+ const scanned = await this.qualityRunner.scan(workspace);
1251
+ const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
1252
+ spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
1253
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
1254
+ }
813
1255
  }
814
1256
 
815
1257
  // pkgs/@akanjs/cli/quality/quality.command.ts
@@ -817,15 +1259,18 @@ class QualityCommand extends command("quality", [QualityScript], ({ public: targ
817
1259
  quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
818
1260
  desc: "quality action",
819
1261
  default: "scan",
820
- enum: ["scan"]
1262
+ enum: ["scan", "ssr"]
821
1263
  }).option("format", String, {
822
1264
  desc: "output format",
823
1265
  default: "text",
824
1266
  enum: ["text", "json"]
825
1267
  }).with(Workspace).exec(async function(action, format, workspace) {
826
- if (action !== "scan")
827
- throw new Error(`Unknown quality action: ${action}. Use "scan".`);
828
- await this.qualityScript.scan(workspace, format);
1268
+ if (action === "scan")
1269
+ await this.qualityScript.scan(workspace, format);
1270
+ else if (action === "ssr")
1271
+ await this.qualityScript.ssr(workspace, format);
1272
+ else
1273
+ throw new Error(`Unknown quality action: ${action}. Use "scan" or "ssr".`);
829
1274
  })
830
1275
  })) {
831
1276
  }