@akanjs/cli 2.4.0 → 2.4.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/agent.command-h4afc69n.js +26 -0
  2. package/application.command-47mj9qsy.js +165 -0
  3. package/applicationBuildRunner-yz144508.js +284 -0
  4. package/applicationReleasePackager-brvth6rs.js +245 -0
  5. package/capacitorApp-y0h6cgft.js +58 -0
  6. package/cloud.command-qgjxja3n.js +94 -0
  7. package/commandManifest.json +1 -0
  8. package/context.command-nqbtak4f.js +82 -0
  9. package/dependencyScanner-m4x5maek.js +9 -0
  10. package/guideline.command-ya0dh44f.js +356 -0
  11. package/incrementalBuilder.proc.js +89 -17394
  12. package/index-1xdrsbry.js +1447 -0
  13. package/index-45aj5ry0.js +990 -0
  14. package/index-5vvwc0cz.js +559 -0
  15. package/index-61keag0s.js +40 -0
  16. package/index-6pz1j0zj.js +62 -0
  17. package/index-73pr2cmy.js +534 -0
  18. package/index-76rn3g2c.js +76 -0
  19. package/index-77crfweb.js +1884 -0
  20. package/index-85msc0wg.js +161 -0
  21. package/index-8pkbzj26.js +840 -0
  22. package/index-8rc0bm04.js +514 -0
  23. package/index-a5rmdgy4.js +4359 -0
  24. package/index-a6sbyy0b.js +2769 -0
  25. package/index-fgc8r6dj.js +33 -0
  26. package/index-h6ca6qg0.js +2777 -0
  27. package/index-hdqztm58.js +758 -0
  28. package/index-hwzpw9c1.js +202 -0
  29. package/index-pmm9e2jf.js +120 -0
  30. package/index-qaq13qk3.js +80 -0
  31. package/index-qhtr07v8.js +1072 -0
  32. package/index-r24hmh0q.js +4 -0
  33. package/index-sgmas1fc.js +462 -0
  34. package/index-ss469dec.js +11 -0
  35. package/index-swf4bmbg.js +25 -0
  36. package/index-wnp7hwq7.js +193 -0
  37. package/index-wq8jwx8z.js +224 -0
  38. package/index-x53a5nya.js +301 -0
  39. package/index-y3hdhy4p.js +229 -0
  40. package/index-z9gvz7b0.js +83 -0
  41. package/index.js +54 -23340
  42. package/library.command-r15zdqvp.js +33 -0
  43. package/localRegistry.command-p1pgxw78.js +178 -0
  44. package/module.command-qrj3kmyz.js +54 -0
  45. package/package.command-r8sq5kzp.js +43 -0
  46. package/package.json +2 -2
  47. package/page.command-c6xdx0xm.js +24 -0
  48. package/primitive.command-pv9ssmtf.js +62 -0
  49. package/quality.command-es67wvdp.js +809 -0
  50. package/repair.command-677675vw.js +67 -0
  51. package/routeSourceValidator-wbhmbwpj.js +132 -0
  52. package/scalar.command-kabkd6wd.js +35 -0
  53. package/templates/facetIndex/index.ts +1 -1
  54. package/typeChecker-kravn7ns.js +8 -0
  55. package/typecheck.proc.js +4 -194
  56. package/workflow.command-64r6cw0w.js +108 -0
  57. package/workspace.command-xk68sd6c.js +435 -0
@@ -0,0 +1,809 @@
1
+ // @bun
2
+ import {
3
+ Workspace,
4
+ command,
5
+ runner,
6
+ script
7
+ } from "./index-hdqztm58.js";
8
+ import"./index-a6sbyy0b.js";
9
+ import"./index-61keag0s.js";
10
+ import"./index-6pz1j0zj.js";
11
+ import"./index-r24hmh0q.js";
12
+
13
+ // pkgs/@akanjs/devkit/qualityScanner.ts
14
+ import { createHash } from "crypto";
15
+ import { readdir, readFile, stat } from "fs/promises";
16
+ import path from "path";
17
+ import ignore from "ignore";
18
+ import ts from "typescript";
19
+ var MAX_FILE_LINES = 2000;
20
+ var PLACEHOLDER_EXPORT_NAMES = new Set([
21
+ "aa",
22
+ "dumb",
23
+ "dumb2",
24
+ "someBaseLogic",
25
+ "someCommonLogic",
26
+ "someFrontendLogic",
27
+ "someBackendLogic"
28
+ ]);
29
+ var SUGGESTED_RULES = [
30
+ "Keep generated scanSync index files out of hand-written changes; generated indexes should only contain one-depth export statements.",
31
+ "Generated Akan index files should not contain placeholder exports such as aa, dumb, dumb2, or some*Logic.",
32
+ "Dictionary text should not contain scaffold wording, misspellings, or stale copied domain nouns.",
33
+ "Warn earlier on very long Akan files: 500 lines for services, 800 lines for Template/Zone files, and 1000 lines for Util files.",
34
+ "Global declarations, Window augmentation, and prototype mutation should stay in explicitly approved low-level integration files.",
35
+ "Keep app root folders small and conventional: application code belongs under common, env, lib, page, plugin, private, public, script, srvkit, ui, or webkit.",
36
+ "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.",
37
+ "Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
38
+ "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.",
39
+ "Move shared app utilities to apps/*/common instead of creating apps/*/base.",
40
+ "Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
41
+ ];
42
+ var APP_ROOT_FILES = new Set([
43
+ "akan.app.json",
44
+ "akan.config.ts",
45
+ "capacitor.config.ts",
46
+ "client.ts",
47
+ "main.ts",
48
+ "package.json",
49
+ "server.ts",
50
+ "tsconfig.json"
51
+ ]);
52
+ var LIB_ROOT_FILES = new Set([
53
+ "cnst.ts",
54
+ "db.ts",
55
+ "dict.ts",
56
+ "option.ts",
57
+ "sig.ts",
58
+ "srv.ts",
59
+ "st.ts",
60
+ "useClient.ts",
61
+ "useServer.ts"
62
+ ]);
63
+ var CONVENTION_SUFFIXES = [
64
+ ".constant.ts",
65
+ ".dictionary.ts",
66
+ ".document.ts",
67
+ ".service.ts",
68
+ ".signal.ts",
69
+ ".store.ts"
70
+ ];
71
+ var PAGE_RESERVED_EXPORTS = new Set([
72
+ "pageConfig",
73
+ "head",
74
+ "metadata",
75
+ "generateHead",
76
+ "generateMetadata",
77
+ "fonts",
78
+ "manifest",
79
+ "theme",
80
+ "reconnect",
81
+ "wsConnect",
82
+ "layoutStyle",
83
+ "gaTrackingId"
84
+ ]);
85
+ var RULE_FIXES = {
86
+ "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.",
87
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
88
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
89
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
90
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
91
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
92
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
93
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
94
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
95
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
96
+ "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`.",
97
+ "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.",
98
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
99
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
100
+ "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."
101
+ };
102
+ function getRuleFix(rule) {
103
+ if (rule.startsWith("akan.convention"))
104
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
105
+ return RULE_FIXES[rule];
106
+ }
107
+
108
+ class AkanQualityScanner {
109
+ async scan(workspaceRoot) {
110
+ const targetFiles = await this.#collectTargetFiles(workspaceRoot);
111
+ const sourceFiles = await Promise.all(targetFiles.map((file) => this.#readSourceFile(workspaceRoot, file)));
112
+ const warnings = [
113
+ ...this.#scanGlobalQuality(sourceFiles),
114
+ ...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
115
+ ...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
116
+ ...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
117
+ ...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile))
118
+ ];
119
+ return {
120
+ workspaceRoot,
121
+ scannedFiles: sourceFiles.length,
122
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
123
+ suggestedRules: SUGGESTED_RULES
124
+ };
125
+ }
126
+ async#collectTargetFiles(workspaceRoot) {
127
+ const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
128
+ const files = [];
129
+ for (const targetRoot of ["apps", "libs"]) {
130
+ const absoluteTargetRoot = path.join(workspaceRoot, targetRoot);
131
+ if (!await isDirectory(absoluteTargetRoot))
132
+ continue;
133
+ await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
134
+ }
135
+ return files.sort();
136
+ }
137
+ async#readGitIgnore(workspaceRoot) {
138
+ const gitIgnorePath = path.join(workspaceRoot, ".gitignore");
139
+ if (!await Bun.file(gitIgnorePath).exists())
140
+ return [];
141
+ return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
142
+ }
143
+ async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
144
+ const entries = await readdir(currentPath, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ const absolutePath = path.join(currentPath, entry.name);
147
+ const relativePath = toPosix(path.relative(workspaceRoot, absolutePath));
148
+ if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
149
+ continue;
150
+ if (entry.isDirectory()) {
151
+ await this.#walkTargetFiles(workspaceRoot, absolutePath, ignoreFilter, files);
152
+ continue;
153
+ }
154
+ if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
155
+ files.push(relativePath);
156
+ }
157
+ }
158
+ }
159
+ async#readSourceFile(workspaceRoot, file) {
160
+ const absolutePath = path.join(workspaceRoot, file);
161
+ const content = await readFile(absolutePath, "utf8");
162
+ return {
163
+ file,
164
+ absolutePath,
165
+ content,
166
+ sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, getScriptKind(file))
167
+ };
168
+ }
169
+ #scanGlobalQuality(sourceFiles) {
170
+ const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
171
+ const warnings = [];
172
+ const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
173
+ for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
174
+ if (declarations.length < 2)
175
+ continue;
176
+ warnings.push({
177
+ rule: "akan.global.duplicate-exported-function-name",
178
+ scope: "global",
179
+ severity: "warning",
180
+ message: `Exported function or class name "${name}" is declared in ${declarations.length} files.`,
181
+ locations: declarations.map(({ file, line }) => ({ file, line }))
182
+ });
183
+ }
184
+ const declarationsWithBody = exportedFunctionLikes.filter((declaration) => declaration.bodyFingerprint);
185
+ for (const [fingerprint, declarations] of groupBy(declarationsWithBody, (declaration) => declaration.bodyFingerprint ?? "")) {
186
+ const uniqueNames = new Set(declarations.map((declaration) => declaration.name));
187
+ if (declarations.length < 2 || uniqueNames.size < 2 || fingerprint === "")
188
+ continue;
189
+ warnings.push({
190
+ rule: "akan.global.duplicate-exported-function-body",
191
+ scope: "global",
192
+ severity: "warning",
193
+ message: `Exported functions/classes share the same implementation body: ${declarations.map((declaration) => declaration.name).join(", ")}.`,
194
+ locations: declarations.map(({ file, line }) => ({ file, line }))
195
+ });
196
+ }
197
+ return warnings;
198
+ }
199
+ #scanSingleFileQuality(sourceFile) {
200
+ const warnings = [];
201
+ const lineCount = sourceFile.content.split(/\r?\n/).length;
202
+ const recommendedLineLimit = getRecommendedLineLimit(sourceFile.file);
203
+ if (recommendedLineLimit && lineCount > recommendedLineLimit) {
204
+ warnings.push({
205
+ rule: "akan.file.recommended-max-lines",
206
+ scope: "file",
207
+ severity: "warning",
208
+ file: sourceFile.file,
209
+ message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
210
+ });
211
+ }
212
+ if (lineCount > MAX_FILE_LINES) {
213
+ warnings.push({
214
+ rule: "akan.file.max-lines",
215
+ scope: "file",
216
+ severity: "warning",
217
+ file: sourceFile.file,
218
+ message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
219
+ });
220
+ }
221
+ warnings.push(...getPlaceholderExportWarnings(sourceFile));
222
+ warnings.push(...getDictionaryTextWarnings(sourceFile));
223
+ warnings.push(...getGlobalMutationWarnings(sourceFile));
224
+ const exportedClassNames = getExportedClassNames(sourceFile.sourceFile);
225
+ if (exportedClassNames.length === 0)
226
+ return warnings;
227
+ const allowedInterfaceNames = new Set(exportedClassNames.map((name) => `${name}Options`));
228
+ for (const declaration of getTopLevelDeclarations(sourceFile)) {
229
+ if (declaration.kind === "class" && exportedClassNames.includes(declaration.name))
230
+ continue;
231
+ if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
232
+ continue;
233
+ warnings.push({
234
+ rule: "akan.file.class-export-global-declaration",
235
+ scope: "file",
236
+ severity: "warning",
237
+ file: sourceFile.file,
238
+ line: declaration.line,
239
+ message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
240
+ });
241
+ }
242
+ return warnings;
243
+ }
244
+ #scanComponentQuality(sourceFile) {
245
+ if (!isComponentDeclarationFile(sourceFile.file))
246
+ return [];
247
+ const isPage = isPageRouteFile(sourceFile.file);
248
+ const declarations = getComponentFileDeclarations(sourceFile.sourceFile);
249
+ const compoundComponentNames = getCompoundComponentNames(sourceFile.sourceFile);
250
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
251
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
252
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
253
+ const warnings = [];
254
+ for (const declaration of declarations) {
255
+ if (declaration.isDefaultExport)
256
+ continue;
257
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
258
+ continue;
259
+ if (declaration.exported) {
260
+ if (isAllowedComponentExport(declaration, isPage))
261
+ continue;
262
+ warnings.push({
263
+ rule: "akan.file.component-export",
264
+ scope: "file",
265
+ severity: "warning",
266
+ file: sourceFile.file,
267
+ line: declaration.line,
268
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
269
+ });
270
+ continue;
271
+ }
272
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
273
+ continue;
274
+ if (isRestrictedInternalKind(declaration.kind)) {
275
+ warnings.push({
276
+ rule: "akan.file.component-internal-declaration",
277
+ scope: "file",
278
+ severity: "warning",
279
+ file: sourceFile.file,
280
+ line: declaration.line,
281
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
282
+ });
283
+ }
284
+ }
285
+ return warnings;
286
+ }
287
+ #scanConventionQuality(sourceFile) {
288
+ const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
289
+ if (!suffix)
290
+ return [];
291
+ const modelName = toPascalCase(path.basename(sourceFile.file, suffix));
292
+ const warnings = [];
293
+ for (const declaration of getTopLevelDeclarations(sourceFile)) {
294
+ if (isAllowedConventionDeclaration(suffix, modelName, declaration))
295
+ continue;
296
+ warnings.push({
297
+ rule: `akan.convention${suffix.replace(".ts", "")}`,
298
+ scope: "convention",
299
+ severity: "warning",
300
+ file: sourceFile.file,
301
+ line: declaration.line,
302
+ message: `${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
303
+ });
304
+ }
305
+ return warnings;
306
+ }
307
+ #scanLayoutQuality(sourceFile) {
308
+ const segments = sourceFile.file.split("/");
309
+ const warnings = [];
310
+ if (segments[0] === "apps" && segments.length === 3 && !APP_ROOT_FILES.has(segments[2])) {
311
+ warnings.push({
312
+ rule: "akan.layout.app-root-file",
313
+ scope: "layout",
314
+ severity: "warning",
315
+ file: sourceFile.file,
316
+ message: `Unexpected app root file "${segments[2]}". Keep application code in conventional app folders.`
317
+ });
318
+ }
319
+ const libRootFile = getLibRootFile(sourceFile.file);
320
+ if (libRootFile && !LIB_ROOT_FILES.has(libRootFile)) {
321
+ warnings.push({
322
+ rule: "akan.layout.lib-root-file",
323
+ scope: "layout",
324
+ severity: "warning",
325
+ file: sourceFile.file,
326
+ message: `Unexpected lib root file "${libRootFile}". Keep direct lib root files limited to generated support facets.`
327
+ });
328
+ }
329
+ const moduleUiWarning = getModuleUiWarning(sourceFile.file);
330
+ if (moduleUiWarning)
331
+ warnings.push(moduleUiWarning);
332
+ return warnings;
333
+ }
334
+ }
335
+ function formatQualityScanResult(result) {
336
+ const sections = [
337
+ "Akan Code Quality Scan",
338
+ `workspace: ${result.workspaceRoot}`,
339
+ `scanned files: ${result.scannedFiles}`,
340
+ `warnings: ${result.warnings.length}`,
341
+ "",
342
+ "Warnings:",
343
+ "",
344
+ ...formatQualityWarnings(result.warnings),
345
+ "",
346
+ "Suggested quality rules:",
347
+ "",
348
+ ...result.suggestedRules.map((rule) => ` - ${rule}`)
349
+ ];
350
+ return sections.join(`
351
+ `);
352
+ }
353
+ function formatQualityWarnings(warnings) {
354
+ if (warnings.length === 0)
355
+ return ["No warnings found."];
356
+ return warnings.flatMap((warning) => {
357
+ const location = formatQualityLocation(warning.file, warning.line);
358
+ const lines = [`${location} - warning ${warning.rule}: ${warning.message}`];
359
+ if (warning.locations?.length) {
360
+ lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
361
+ }
362
+ if (warning.fix)
363
+ lines.push(` fix: ${warning.fix}`);
364
+ return lines;
365
+ });
366
+ }
367
+ function formatQualityLocation(file, line) {
368
+ return `${file ?? "<global>"}:${line ?? 1}:1`;
369
+ }
370
+ function getExportedFunctionLikes(sourceFile) {
371
+ const declarations = [];
372
+ const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
373
+ for (const statement of sourceFile.sourceFile.statements) {
374
+ if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
375
+ declarations.push({
376
+ name: statement.name.text,
377
+ kind: "function",
378
+ file: sourceFile.file,
379
+ line: getLine(sourceFile.sourceFile, statement),
380
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
381
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
382
+ });
383
+ }
384
+ if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
385
+ declarations.push({
386
+ name: statement.name.text,
387
+ kind: "class",
388
+ file: sourceFile.file,
389
+ line: getLine(sourceFile.sourceFile, statement),
390
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
391
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
392
+ });
393
+ }
394
+ if (ts.isVariableStatement(statement) && isExported(statement)) {
395
+ for (const declaration of statement.declarationList.declarations) {
396
+ if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
397
+ continue;
398
+ declarations.push({
399
+ name: declaration.name.text,
400
+ kind: "function-variable",
401
+ file: sourceFile.file,
402
+ line: getLine(sourceFile.sourceFile, declaration),
403
+ bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
404
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
405
+ });
406
+ }
407
+ }
408
+ }
409
+ return declarations;
410
+ }
411
+ function isPageRouteFile(file) {
412
+ const segments = file.split("/");
413
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
414
+ }
415
+ function isUiComponentFile(file) {
416
+ const segments = file.split("/");
417
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
418
+ }
419
+ function isConventionDuplicateNameExempt(file, isEnumClass) {
420
+ if (!isInLibModule(file))
421
+ return false;
422
+ if (file.endsWith(".tsx"))
423
+ return true;
424
+ if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
425
+ return true;
426
+ if (file.endsWith(".constant.ts"))
427
+ return !isEnumClass;
428
+ return false;
429
+ }
430
+ function isInLibModule(file) {
431
+ const segments = file.split("/");
432
+ return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
433
+ }
434
+ function isEnumClassStatement(sourceFile, statement) {
435
+ if (!ts.isClassDeclaration(statement))
436
+ return false;
437
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
438
+ const expression = heritageClause?.types[0]?.expression;
439
+ return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
440
+ }
441
+ function getExportedClassNames(sourceFile) {
442
+ return sourceFile.statements.filter((statement) => ts.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
443
+ }
444
+ function isComponentDeclarationFile(file) {
445
+ if (!file.endsWith(".tsx"))
446
+ return false;
447
+ const segments = file.split("/");
448
+ const [root, , area] = segments;
449
+ if (root !== "apps" && root !== "libs")
450
+ return false;
451
+ if (area === "lib" || area === "ui")
452
+ return true;
453
+ return root === "apps" && area === "page";
454
+ }
455
+ function getComponentFileDeclarations(sourceFile) {
456
+ const reExportedNames = new Set;
457
+ for (const statement of sourceFile.statements) {
458
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
459
+ for (const element of statement.exportClause.elements)
460
+ reExportedNames.add((element.propertyName ?? element.name).text);
461
+ }
462
+ }
463
+ const declarations = [];
464
+ for (const statement of sourceFile.statements) {
465
+ const isDefaultExport = isDefaultExportStatement(statement);
466
+ const inlineExported = isExported(statement);
467
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
468
+ if (ts.isInterfaceDeclaration(statement))
469
+ add(statement.name.text, "interface", getLine(sourceFile, statement));
470
+ else if (ts.isTypeAliasDeclaration(statement))
471
+ add(statement.name.text, "type", getLine(sourceFile, statement));
472
+ else if (ts.isEnumDeclaration(statement))
473
+ add(statement.name.text, "enum", getLine(sourceFile, statement));
474
+ else if (ts.isFunctionDeclaration(statement) && statement.name)
475
+ add(statement.name.text, "function", getLine(sourceFile, statement));
476
+ else if (ts.isClassDeclaration(statement) && statement.name)
477
+ add(statement.name.text, "class", getLine(sourceFile, statement));
478
+ else if (ts.isVariableStatement(statement)) {
479
+ for (const declaration of statement.declarationList.declarations) {
480
+ if (!ts.isIdentifier(declaration.name))
481
+ continue;
482
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
483
+ add(declaration.name.text, kind, getLine(sourceFile, declaration));
484
+ }
485
+ }
486
+ }
487
+ return declarations;
488
+ }
489
+ function getCompoundComponentNames(sourceFile) {
490
+ const names = new Set;
491
+ for (const statement of sourceFile.statements) {
492
+ if (!ts.isExpressionStatement(statement))
493
+ continue;
494
+ const { expression } = statement;
495
+ if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
496
+ continue;
497
+ if (!ts.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
498
+ continue;
499
+ names.add(expression.left.name.text);
500
+ if (ts.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
501
+ names.add(expression.right.text);
502
+ }
503
+ return names;
504
+ }
505
+ function isComponentValueKind(kind) {
506
+ return kind === "variable" || kind === "function" || kind === "class";
507
+ }
508
+ function isRestrictedInternalKind(kind) {
509
+ return kind === "interface" || kind === "type" || kind === "function";
510
+ }
511
+ function isAllowedComponentExport(declaration, isPage) {
512
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
513
+ return true;
514
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
515
+ }
516
+ function isPascalCaseName(name) {
517
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
518
+ }
519
+ function isDefaultExportStatement(statement) {
520
+ if (ts.isExportAssignment(statement))
521
+ return !statement.isExportEquals;
522
+ return !!(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Default);
523
+ }
524
+ function getPlaceholderExportWarnings(sourceFile) {
525
+ if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
526
+ return [];
527
+ return getTopLevelDeclarations(sourceFile).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
528
+ rule: "akan.file.placeholder-export",
529
+ scope: "file",
530
+ severity: "warning",
531
+ file: sourceFile.file,
532
+ line: declaration.line,
533
+ message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
534
+ }));
535
+ }
536
+ function getDictionaryTextWarnings(sourceFile) {
537
+ if (!sourceFile.file.endsWith(".dictionary.ts"))
538
+ return [];
539
+ const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
540
+ return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile.content, pattern).map((line) => ({
541
+ rule: "akan.file.dictionary-stale-text",
542
+ scope: "file",
543
+ severity: "warning",
544
+ file: sourceFile.file,
545
+ line,
546
+ message: `Dictionary text appears to contain ${label}.`
547
+ })));
548
+ }
549
+ function getGlobalMutationWarnings(sourceFile) {
550
+ const warnings = [];
551
+ for (const statement of sourceFile.sourceFile.statements) {
552
+ if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
553
+ warnings.push({
554
+ rule: "akan.file.global-declaration",
555
+ scope: "file",
556
+ severity: "warning",
557
+ file: sourceFile.file,
558
+ line: getLine(sourceFile.sourceFile, statement),
559
+ message: "Global declarations require an explicit low-level integration allowlist."
560
+ });
561
+ }
562
+ if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
563
+ warnings.push({
564
+ rule: "akan.file.window-augmentation",
565
+ scope: "file",
566
+ severity: "warning",
567
+ file: sourceFile.file,
568
+ line: getLine(sourceFile.sourceFile, statement),
569
+ message: "Window augmentation should be isolated to approved browser integration files."
570
+ });
571
+ }
572
+ if (ts.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
573
+ warnings.push({
574
+ rule: "akan.file.prototype-mutation",
575
+ scope: "file",
576
+ severity: "warning",
577
+ file: sourceFile.file,
578
+ line: getLine(sourceFile.sourceFile, statement),
579
+ message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
580
+ });
581
+ }
582
+ }
583
+ return warnings;
584
+ }
585
+ function getTopLevelDeclarations(sourceFile) {
586
+ return sourceFile.sourceFile.statements.flatMap((statement) => getTopLevelDeclaration(sourceFile.sourceFile, statement));
587
+ }
588
+ function getTopLevelDeclaration(sourceFile, statement) {
589
+ const line = getLine(sourceFile, statement);
590
+ if (ts.isClassDeclaration(statement) && statement.name) {
591
+ return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
592
+ }
593
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
594
+ return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
595
+ }
596
+ if (ts.isInterfaceDeclaration(statement)) {
597
+ return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
598
+ }
599
+ if (ts.isTypeAliasDeclaration(statement)) {
600
+ return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
601
+ }
602
+ if (ts.isEnumDeclaration(statement)) {
603
+ return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
604
+ }
605
+ if (ts.isVariableStatement(statement)) {
606
+ return statement.declarationList.declarations.filter((declaration) => ts.isIdentifier(declaration.name)).map((declaration) => ({
607
+ name: declaration.name.text,
608
+ kind: "variable",
609
+ line: getLine(sourceFile, declaration),
610
+ exported: isExported(statement),
611
+ node: statement
612
+ }));
613
+ }
614
+ if (ts.isExportDeclaration(statement)) {
615
+ return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
616
+ }
617
+ return [];
618
+ }
619
+ function isAllowedConventionDeclaration(suffix, modelName, declaration) {
620
+ if (suffix === ".dictionary.ts")
621
+ return isExportedConst(declaration) && declaration.name === "dictionary";
622
+ if (suffix === ".constant.ts")
623
+ return isAllowedConstantDeclaration(modelName, declaration);
624
+ if (suffix === ".document.ts")
625
+ return declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name);
626
+ if (suffix === ".service.ts")
627
+ return declaration.kind === "class" && declaration.name === `${modelName}Service`;
628
+ if (suffix === ".signal.ts")
629
+ return declaration.kind === "class" && [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name);
630
+ if (suffix === ".store.ts")
631
+ return declaration.kind === "class" && declaration.name === `${modelName}Store`;
632
+ return false;
633
+ }
634
+ function isAllowedConstantDeclaration(modelName, declaration) {
635
+ if (declaration.kind !== "class")
636
+ return false;
637
+ if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
638
+ return true;
639
+ if (!ts.isClassDeclaration(declaration.node))
640
+ return false;
641
+ const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
642
+ const expression = heritageClause?.types[0]?.expression;
643
+ return !!expression && expression.getText().startsWith("enumOf(");
644
+ }
645
+ function getConventionDescription(suffix, modelName) {
646
+ if (suffix === ".dictionary.ts")
647
+ return "export const dictionary";
648
+ if (suffix === ".constant.ts")
649
+ return `${modelName}Input, ${modelName}Object, ${modelName}, Light${modelName}, ${modelName}Insight, or enumOf classes`;
650
+ if (suffix === ".document.ts")
651
+ return `${modelName}Filter, ${modelName}, or ${modelName}Model`;
652
+ if (suffix === ".service.ts")
653
+ return `${modelName}Service`;
654
+ if (suffix === ".signal.ts")
655
+ return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
656
+ return `${modelName}Store`;
657
+ }
658
+ function getLibRootFile(file) {
659
+ const segments = file.split("/");
660
+ if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
661
+ return segments[3];
662
+ return null;
663
+ }
664
+ function getModuleUiWarning(file) {
665
+ if (!file.endsWith(".tsx"))
666
+ return null;
667
+ const moduleInfo = getModuleInfo(file);
668
+ if (!moduleInfo)
669
+ return null;
670
+ const { moduleName, fileName, kind } = moduleInfo;
671
+ const pascalName = toPascalCase(moduleName.replace(/^_+/, ""));
672
+ const allowedSuffixes = kind === "database" ? ["Template", "Unit", "Util", "View", "Zone"] : kind === "service" ? ["Util", "Zone"] : ["Template", "Unit"];
673
+ const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
674
+ if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
675
+ return null;
676
+ return {
677
+ rule: "akan.layout.module-ui-file",
678
+ scope: "layout",
679
+ severity: "warning",
680
+ file,
681
+ message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
682
+ };
683
+ }
684
+ function getModuleInfo(file) {
685
+ const segments = file.split("/");
686
+ const libIndex = segments.indexOf("lib");
687
+ if (libIndex < 0)
688
+ return null;
689
+ const moduleName = segments[libIndex + 1];
690
+ if (moduleName === "__scalar") {
691
+ if (segments.length !== libIndex + 4)
692
+ return null;
693
+ return { moduleName: segments[libIndex + 2], fileName: segments[libIndex + 3], kind: "scalar" };
694
+ }
695
+ if (segments.length !== libIndex + 3)
696
+ return null;
697
+ const fileName = segments[libIndex + 2];
698
+ if (moduleName.startsWith("__")) {
699
+ const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
700
+ return { moduleName: scalarName, fileName, kind: "scalar" };
701
+ }
702
+ if (moduleName.startsWith("_"))
703
+ return { moduleName, fileName, kind: "service" };
704
+ return { moduleName, fileName, kind: "database" };
705
+ }
706
+ function isExportedConst(declaration) {
707
+ return declaration.exported && ts.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts.NodeFlags.Const) !== 0;
708
+ }
709
+ function isExported(node) {
710
+ return !!ts.getCombinedModifierFlags(node) && !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
711
+ }
712
+ function isFunctionLikeInitializer(node) {
713
+ return !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
714
+ }
715
+ function getBodyFingerprint(sourceFile, node) {
716
+ if (!node)
717
+ return;
718
+ const normalizedBody = node.getText(sourceFile).replace(/\s+/g, " ").trim();
719
+ if (normalizedBody.length < 80)
720
+ return;
721
+ return createHash("sha256").update(normalizedBody).digest("hex");
722
+ }
723
+ function shouldSkipPath(relativePath, isDirectory, ignoreFilter) {
724
+ const ignorePath = isDirectory ? `${relativePath}/` : relativePath;
725
+ return relativePath === ".git" || relativePath.includes("/.git/") || relativePath.includes("/node_modules/") || ignoreFilter.ignores(relativePath) || ignoreFilter.ignores(ignorePath);
726
+ }
727
+ async function isDirectory(absolutePath) {
728
+ try {
729
+ return (await stat(absolutePath)).isDirectory();
730
+ } catch {
731
+ return false;
732
+ }
733
+ }
734
+ function getScriptKind(file) {
735
+ return file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
736
+ }
737
+ function getLine(sourceFile, node) {
738
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
739
+ }
740
+ function getRecommendedLineLimit(file) {
741
+ if (file.endsWith(".service.ts"))
742
+ return 500;
743
+ if (file.endsWith(".Template.tsx") || file.endsWith(".Zone.tsx"))
744
+ return 800;
745
+ if (file.endsWith(".Util.tsx"))
746
+ return 1000;
747
+ return null;
748
+ }
749
+ function findPatternLines(content, pattern) {
750
+ return content.split(/\r?\n/).flatMap((line, index) => pattern.test(line) ? [index + 1] : []);
751
+ }
752
+ function toPascalCase(value) {
753
+ return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
754
+ }
755
+ function toPosix(value) {
756
+ return value.split(path.sep).join("/");
757
+ }
758
+ function groupBy(items, getKey) {
759
+ const grouped = new Map;
760
+ for (const item of items) {
761
+ const key = getKey(item);
762
+ grouped.set(key, [...grouped.get(key) ?? [], item]);
763
+ }
764
+ return grouped;
765
+ }
766
+ function compareWarnings(a, b) {
767
+ return a.scope.localeCompare(b.scope) || (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule);
768
+ }
769
+
770
+ // pkgs/@akanjs/cli/quality/quality.script.ts
771
+ import { Logger } from "akanjs/common";
772
+
773
+ // pkgs/@akanjs/cli/quality/quality.runner.ts
774
+ class QualityRunner extends runner("quality") {
775
+ async scan(workspace) {
776
+ return await new AkanQualityScanner().scan(workspace.workspaceRoot);
777
+ }
778
+ }
779
+
780
+ // pkgs/@akanjs/cli/quality/quality.script.ts
781
+ class QualityScript extends script("quality", [QualityRunner]) {
782
+ async scan(workspace, format = "text") {
783
+ const spinner = workspace.spinning("Scanning Akan code quality...");
784
+ const result = await this.qualityRunner.scan(workspace);
785
+ spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
786
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
787
+ }
788
+ }
789
+
790
+ // pkgs/@akanjs/cli/quality/quality.command.ts
791
+ class QualityCommand extends command("quality", [QualityScript], ({ public: target }) => ({
792
+ quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
793
+ desc: "quality action",
794
+ default: "scan",
795
+ enum: ["scan"]
796
+ }).option("format", String, {
797
+ desc: "output format",
798
+ default: "text",
799
+ enum: ["text", "json"]
800
+ }).with(Workspace).exec(async function(action, format, workspace) {
801
+ if (action !== "scan")
802
+ throw new Error(`Unknown quality action: ${action}. Use "scan".`);
803
+ await this.qualityScript.scan(workspace, format);
804
+ })
805
+ })) {
806
+ }
807
+ export {
808
+ QualityCommand
809
+ };