@rigour-labs/core 1.6.0 → 2.0.0

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 (40) hide show
  1. package/dist/discovery.js +12 -19
  2. package/dist/gates/ast-handlers/base.d.ts +12 -0
  3. package/dist/gates/ast-handlers/base.js +6 -0
  4. package/dist/gates/ast-handlers/python.d.ts +6 -0
  5. package/dist/gates/ast-handlers/python.js +64 -0
  6. package/dist/gates/ast-handlers/typescript.d.ts +9 -0
  7. package/dist/gates/ast-handlers/typescript.js +110 -0
  8. package/dist/gates/ast-handlers/universal.d.ts +8 -0
  9. package/dist/gates/ast-handlers/universal.js +156 -0
  10. package/dist/gates/ast.d.ts +1 -3
  11. package/dist/gates/ast.js +30 -109
  12. package/dist/gates/base.js +1 -5
  13. package/dist/gates/content.js +5 -9
  14. package/dist/gates/coverage.d.ts +8 -0
  15. package/dist/gates/coverage.js +62 -0
  16. package/dist/gates/dependency.js +7 -14
  17. package/dist/gates/file.js +5 -9
  18. package/dist/gates/runner.js +22 -22
  19. package/dist/gates/safety.js +4 -8
  20. package/dist/gates/structure.js +6 -13
  21. package/dist/index.js +8 -26
  22. package/dist/services/fix-packet-service.js +3 -7
  23. package/dist/services/state-service.js +9 -16
  24. package/dist/smoke.test.js +6 -8
  25. package/dist/templates/index.js +3 -6
  26. package/dist/types/fix-packet.js +22 -25
  27. package/dist/types/index.d.ts +5 -0
  28. package/dist/types/index.js +54 -56
  29. package/dist/utils/logger.js +8 -15
  30. package/dist/utils/scanner.js +7 -14
  31. package/package.json +3 -1
  32. package/src/gates/ast-handlers/base.ts +13 -0
  33. package/src/gates/ast-handlers/python.ts +71 -0
  34. package/src/gates/ast-handlers/python_parser.py +60 -0
  35. package/src/gates/ast-handlers/typescript.ts +125 -0
  36. package/src/gates/ast-handlers/universal.ts +184 -0
  37. package/src/gates/ast.ts +27 -127
  38. package/src/gates/coverage.ts +70 -0
  39. package/src/gates/runner.ts +4 -0
  40. package/src/types/index.ts +1 -0
@@ -1,73 +1,71 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ReportSchema = exports.FailureSchema = exports.StatusSchema = exports.ConfigSchema = exports.CommandsSchema = exports.GatesSchema = void 0;
4
- const zod_1 = require("zod");
5
- exports.GatesSchema = zod_1.z.object({
6
- max_file_lines: zod_1.z.number().optional().default(500),
7
- forbid_todos: zod_1.z.boolean().optional().default(true),
8
- forbid_fixme: zod_1.z.boolean().optional().default(true),
9
- forbid_paths: zod_1.z.array(zod_1.z.string()).optional().default([]),
10
- required_files: zod_1.z.array(zod_1.z.string()).optional().default([
1
+ import { z } from 'zod';
2
+ export const GatesSchema = z.object({
3
+ max_file_lines: z.number().optional().default(500),
4
+ forbid_todos: z.boolean().optional().default(true),
5
+ forbid_fixme: z.boolean().optional().default(true),
6
+ forbid_paths: z.array(z.string()).optional().default([]),
7
+ required_files: z.array(z.string()).optional().default([
11
8
  'docs/SPEC.md',
12
9
  'docs/ARCH.md',
13
10
  'docs/DECISIONS.md',
14
11
  'docs/TASKS.md',
15
12
  ]),
16
- ast: zod_1.z.object({
17
- complexity: zod_1.z.number().optional().default(10),
18
- max_methods: zod_1.z.number().optional().default(10),
19
- max_params: zod_1.z.number().optional().default(5),
20
- max_nesting: zod_1.z.number().optional().default(4),
21
- max_inheritance_depth: zod_1.z.number().optional().default(3),
22
- max_class_dependencies: zod_1.z.number().optional().default(5),
23
- max_function_lines: zod_1.z.number().optional().default(50),
13
+ ast: z.object({
14
+ complexity: z.number().optional().default(10),
15
+ max_methods: z.number().optional().default(10),
16
+ max_params: z.number().optional().default(5),
17
+ max_nesting: z.number().optional().default(4),
18
+ max_inheritance_depth: z.number().optional().default(3),
19
+ max_class_dependencies: z.number().optional().default(5),
20
+ max_function_lines: z.number().optional().default(50),
24
21
  }).optional().default({}),
25
- dependencies: zod_1.z.object({
26
- forbid: zod_1.z.array(zod_1.z.string()).optional().default([]),
27
- trusted_registry: zod_1.z.string().optional(),
22
+ dependencies: z.object({
23
+ forbid: z.array(z.string()).optional().default([]),
24
+ trusted_registry: z.string().optional(),
28
25
  }).optional().default({}),
29
- architecture: zod_1.z.object({
30
- boundaries: zod_1.z.array(zod_1.z.object({
31
- from: zod_1.z.string(),
32
- to: zod_1.z.string(),
33
- mode: zod_1.z.enum(['allow', 'deny']).default('deny'),
26
+ architecture: z.object({
27
+ boundaries: z.array(z.object({
28
+ from: z.string(),
29
+ to: z.string(),
30
+ mode: z.enum(['allow', 'deny']).default('deny'),
34
31
  })).optional().default([]),
35
32
  }).optional().default({}),
36
- safety: zod_1.z.object({
37
- max_files_changed_per_cycle: zod_1.z.number().optional().default(10),
38
- protected_paths: zod_1.z.array(zod_1.z.string()).optional().default(['.github/**', 'docs/**', 'rigour.yml']),
33
+ safety: z.object({
34
+ max_files_changed_per_cycle: z.number().optional().default(10),
35
+ protected_paths: z.array(z.string()).optional().default(['.github/**', 'docs/**', 'rigour.yml']),
39
36
  }).optional().default({}),
40
37
  });
41
- exports.CommandsSchema = zod_1.z.object({
42
- format: zod_1.z.string().optional(),
43
- lint: zod_1.z.string().optional(),
44
- typecheck: zod_1.z.string().optional(),
45
- test: zod_1.z.string().optional(),
38
+ export const CommandsSchema = z.object({
39
+ format: z.string().optional(),
40
+ lint: z.string().optional(),
41
+ typecheck: z.string().optional(),
42
+ test: z.string().optional(),
46
43
  });
47
- exports.ConfigSchema = zod_1.z.object({
48
- version: zod_1.z.number().default(1),
49
- preset: zod_1.z.string().optional(),
50
- paradigm: zod_1.z.string().optional(),
51
- commands: exports.CommandsSchema.optional().default({}),
52
- gates: exports.GatesSchema.optional().default({}),
53
- output: zod_1.z.object({
54
- report_path: zod_1.z.string().default('rigour-report.json'),
44
+ export const ConfigSchema = z.object({
45
+ version: z.number().default(1),
46
+ preset: z.string().optional(),
47
+ paradigm: z.string().optional(),
48
+ commands: CommandsSchema.optional().default({}),
49
+ gates: GatesSchema.optional().default({}),
50
+ output: z.object({
51
+ report_path: z.string().default('rigour-report.json'),
55
52
  }).optional().default({}),
56
- planned: zod_1.z.array(zod_1.z.string()).optional().default([]),
53
+ planned: z.array(z.string()).optional().default([]),
57
54
  });
58
- exports.StatusSchema = zod_1.z.enum(['PASS', 'FAIL', 'SKIP', 'ERROR']);
59
- exports.FailureSchema = zod_1.z.object({
60
- id: zod_1.z.string(),
61
- title: zod_1.z.string(),
62
- details: zod_1.z.string(),
63
- files: zod_1.z.array(zod_1.z.string()).optional(),
64
- hint: zod_1.z.string().optional(),
55
+ export const StatusSchema = z.enum(['PASS', 'FAIL', 'SKIP', 'ERROR']);
56
+ export const FailureSchema = z.object({
57
+ id: z.string(),
58
+ title: z.string(),
59
+ details: z.string(),
60
+ files: z.array(z.string()).optional(),
61
+ hint: z.string().optional(),
65
62
  });
66
- exports.ReportSchema = zod_1.z.object({
67
- status: exports.StatusSchema,
68
- summary: zod_1.z.record(exports.StatusSchema),
69
- failures: zod_1.z.array(exports.FailureSchema),
70
- stats: zod_1.z.object({
71
- duration_ms: zod_1.z.number(),
63
+ export const ReportSchema = z.object({
64
+ status: StatusSchema,
65
+ summary: z.record(StatusSchema),
66
+ failures: z.array(FailureSchema),
67
+ stats: z.object({
68
+ duration_ms: z.number(),
69
+ score: z.number().optional(),
72
70
  }),
73
71
  });
@@ -1,35 +1,29 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Logger = exports.LogLevel = void 0;
7
- const chalk_1 = __importDefault(require("chalk"));
8
- var LogLevel;
1
+ import chalk from 'chalk';
2
+ export var LogLevel;
9
3
  (function (LogLevel) {
10
4
  LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
11
5
  LogLevel[LogLevel["INFO"] = 1] = "INFO";
12
6
  LogLevel[LogLevel["WARN"] = 2] = "WARN";
13
7
  LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
14
- })(LogLevel || (exports.LogLevel = LogLevel = {}));
15
- class Logger {
8
+ })(LogLevel || (LogLevel = {}));
9
+ export class Logger {
16
10
  static level = LogLevel.INFO;
17
11
  static setLevel(level) {
18
12
  this.level = level;
19
13
  }
20
14
  static info(message) {
21
15
  if (this.level <= LogLevel.INFO) {
22
- console.log(chalk_1.default.blue('info: ') + message);
16
+ console.log(chalk.blue('info: ') + message);
23
17
  }
24
18
  }
25
19
  static warn(message) {
26
20
  if (this.level <= LogLevel.WARN) {
27
- console.log(chalk_1.default.yellow('warn: ') + message);
21
+ console.log(chalk.yellow('warn: ') + message);
28
22
  }
29
23
  }
30
24
  static error(message, error) {
31
25
  if (this.level <= LogLevel.ERROR) {
32
- console.error(chalk_1.default.red('error: ') + message);
26
+ console.error(chalk.red('error: ') + message);
33
27
  if (error) {
34
28
  console.error(error);
35
29
  }
@@ -37,8 +31,7 @@ class Logger {
37
31
  }
38
32
  static debug(message) {
39
33
  if (this.level <= LogLevel.DEBUG) {
40
- console.log(chalk_1.default.dim('debug: ') + message);
34
+ console.log(chalk.dim('debug: ') + message);
41
35
  }
42
36
  }
43
37
  }
44
- exports.Logger = Logger;
@@ -1,13 +1,7 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FileScanner = void 0;
7
- const globby_1 = require("globby");
8
- const fs_extra_1 = __importDefault(require("fs-extra"));
9
- const path_1 = __importDefault(require("path"));
10
- class FileScanner {
1
+ import { globby } from 'globby';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ export class FileScanner {
11
5
  static DEFAULT_PATTERNS = ['**/*.{ts,js,py,css,html,md}'];
12
6
  static DEFAULT_IGNORE = [
13
7
  '**/node_modules/**',
@@ -18,7 +12,7 @@ class FileScanner {
18
12
  'rigour-report.json'
19
13
  ];
20
14
  static async findFiles(options) {
21
- return (0, globby_1.globby)(options.patterns || this.DEFAULT_PATTERNS, {
15
+ return globby(options.patterns || this.DEFAULT_PATTERNS, {
22
16
  cwd: options.cwd,
23
17
  ignore: options.ignore || this.DEFAULT_IGNORE,
24
18
  });
@@ -26,10 +20,9 @@ class FileScanner {
26
20
  static async readFiles(cwd, files) {
27
21
  const contents = new Map();
28
22
  for (const file of files) {
29
- const filePath = path_1.default.join(cwd, file);
30
- contents.set(file, await fs_extra_1.default.readFile(filePath, 'utf-8'));
23
+ const filePath = path.join(cwd, file);
24
+ contents.set(file, await fs.readFile(filePath, 'utf-8'));
31
25
  }
32
26
  return contents;
33
27
  }
34
28
  }
35
- exports.FileScanner = FileScanner;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
+ "type": "module",
4
5
  "main": "dist/index.js",
5
6
  "types": "dist/index.d.ts",
6
7
  "repository": {
@@ -18,6 +19,7 @@
18
19
  "globby": "^14.1.0",
19
20
  "micromatch": "^4.0.8",
20
21
  "typescript": "^5.9.3",
22
+ "web-tree-sitter": "^0.26.3",
21
23
  "yaml": "^2.3.4",
22
24
  "zod": "^3.22.4"
23
25
  },
@@ -0,0 +1,13 @@
1
+ import { Failure, Gates } from '../../types/index.js';
2
+
3
+ export interface ASTHandlerContext {
4
+ cwd: string;
5
+ file: string;
6
+ content: string;
7
+ }
8
+
9
+ export abstract class ASTHandler {
10
+ constructor(protected config: Gates) { }
11
+ abstract supports(file: string): boolean;
12
+ abstract run(context: ASTHandlerContext): Promise<Failure[]>;
13
+ }
@@ -0,0 +1,71 @@
1
+ import { execa } from 'execa';
2
+ import { ASTHandler, ASTHandlerContext } from './base.js';
3
+ import { Failure } from '../../types/index.js';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ export class PythonHandler extends ASTHandler {
10
+ supports(file: string): boolean {
11
+ return /\.py$/.test(file);
12
+ }
13
+
14
+ async run(context: ASTHandlerContext): Promise<Failure[]> {
15
+ const failures: Failure[] = [];
16
+ const scriptPath = path.join(__dirname, 'python_parser.py');
17
+
18
+ try {
19
+ const { stdout } = await execa('python3', [scriptPath], {
20
+ input: context.content,
21
+ cwd: context.cwd
22
+ });
23
+
24
+ const metrics = JSON.parse(stdout);
25
+ if (metrics.error) return [];
26
+
27
+ const astConfig = this.config.ast || {};
28
+ const maxComplexity = astConfig.complexity || 10;
29
+ const maxParams = astConfig.max_params || 5;
30
+ const maxMethods = astConfig.max_methods || 10;
31
+
32
+ for (const item of metrics) {
33
+ if (item.type === 'function') {
34
+ if (item.parameters > maxParams) {
35
+ failures.push({
36
+ id: 'AST_MAX_PARAMS',
37
+ title: `Function '${item.name}' has ${item.parameters} parameters (max: ${maxParams})`,
38
+ details: `High parameter count detected in ${context.file} at line ${item.lineno}`,
39
+ files: [context.file],
40
+ hint: `Reduce number of parameters or use an options object.`
41
+ });
42
+ }
43
+ if (item.complexity > maxComplexity) {
44
+ failures.push({
45
+ id: 'AST_COMPLEXITY',
46
+ title: `Function '${item.name}' has complexity of ${item.complexity} (max: ${maxComplexity})`,
47
+ details: `High complexity detected in ${context.file} at line ${item.lineno}`,
48
+ files: [context.file],
49
+ hint: `Refactor '${item.name}' into smaller, more focused functions.`
50
+ });
51
+ }
52
+ } else if (item.type === 'class') {
53
+ if (item.methods > maxMethods) {
54
+ failures.push({
55
+ id: 'AST_MAX_METHODS',
56
+ title: `Class '${item.name}' has ${item.methods} methods (max: ${maxMethods})`,
57
+ details: `God Object pattern detected in ${context.file} at line ${item.lineno}`,
58
+ files: [context.file],
59
+ hint: `Split class '${item.name}' into smaller services.`
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ } catch (e: any) {
66
+ // If python3 is missing, we skip AST but other gates still run
67
+ }
68
+
69
+ return failures;
70
+ }
71
+ }
@@ -0,0 +1,60 @@
1
+ import ast
2
+ import sys
3
+ import json
4
+
5
+ class MetricsVisitor(ast.NodeVisitor):
6
+ def __init__(self):
7
+ self.metrics = []
8
+
9
+ def visit_FunctionDef(self, node):
10
+ self.analyze_function(node)
11
+ self.generic_visit(node)
12
+
13
+ def visit_AsyncFunctionDef(self, node):
14
+ self.analyze_function(node)
15
+ self.generic_visit(node)
16
+
17
+ def visit_ClassDef(self, node):
18
+ methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
19
+ self.metrics.append({
20
+ "type": "class",
21
+ "name": node.name,
22
+ "methods": len(methods),
23
+ "lineno": node.lineno
24
+ })
25
+ self.generic_visit(node)
26
+
27
+ def analyze_function(self, node):
28
+ complexity = 1
29
+ for n in ast.walk(node):
30
+ if isinstance(n, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.Try, ast.ExceptHandler, ast.With, ast.AsyncWith)):
31
+ complexity += 1
32
+ elif isinstance(n, ast.BoolOp):
33
+ complexity += len(n.values) - 1
34
+ elif isinstance(n, ast.IfExp):
35
+ complexity += 1
36
+
37
+ params = len(node.args.args) + len(node.args.kwonlyargs)
38
+ if node.args.vararg: params += 1
39
+ if node.args.kwarg: params += 1
40
+
41
+ self.metrics.append({
42
+ "type": "function",
43
+ "name": node.name,
44
+ "complexity": complexity,
45
+ "parameters": params,
46
+ "lineno": node.lineno
47
+ })
48
+
49
+ def analyze_code(content):
50
+ try:
51
+ tree = ast.parse(content)
52
+ visitor = MetricsVisitor()
53
+ visitor.visit(tree)
54
+ return visitor.metrics
55
+ except Exception as e:
56
+ return {"error": str(e)}
57
+
58
+ if __name__ == "__main__":
59
+ content = sys.stdin.read()
60
+ print(json.dumps(analyze_code(content)))
@@ -0,0 +1,125 @@
1
+ import ts from 'typescript';
2
+ import { ASTHandler, ASTHandlerContext } from './base.js';
3
+ import { Failure } from '../../types/index.js';
4
+ import micromatch from 'micromatch';
5
+ import path from 'path';
6
+
7
+ export class TypeScriptHandler extends ASTHandler {
8
+ supports(file: string): boolean {
9
+ return /\.(ts|js|tsx|jsx)$/.test(file);
10
+ }
11
+
12
+ async run(context: ASTHandlerContext): Promise<Failure[]> {
13
+ const failures: Failure[] = [];
14
+ const sourceFile = ts.createSourceFile(context.file, context.content, ts.ScriptTarget.Latest, true);
15
+ this.analyzeSourceFile(sourceFile, context.file, failures);
16
+ return failures;
17
+ }
18
+
19
+ private analyzeSourceFile(sourceFile: ts.SourceFile, relativePath: string, failures: Failure[]) {
20
+ const astConfig = this.config.ast || {};
21
+ const maxComplexity = astConfig.complexity || 10;
22
+ const maxMethods = astConfig.max_methods || 10;
23
+ const maxParams = astConfig.max_params || 5;
24
+
25
+ const visit = (node: ts.Node) => {
26
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
27
+ const name = this.getNodeName(node);
28
+
29
+ if (node.parameters.length > maxParams) {
30
+ failures.push({
31
+ id: 'AST_MAX_PARAMS',
32
+ title: `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
33
+ details: `High parameter count detected in ${relativePath}`,
34
+ files: [relativePath],
35
+ hint: `Reduce number of parameters or use an options object.`
36
+ });
37
+ }
38
+
39
+ let complexity = 1;
40
+ const countComplexity = (n: ts.Node) => {
41
+ if (ts.isIfStatement(n) || ts.isCaseClause(n) || ts.isDefaultClause(n) ||
42
+ ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
43
+ ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isConditionalExpression(n)) {
44
+ complexity++;
45
+ }
46
+ if (ts.isBinaryExpression(n)) {
47
+ if (n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
48
+ n.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
49
+ complexity++;
50
+ }
51
+ }
52
+ ts.forEachChild(n, countComplexity);
53
+ };
54
+ ts.forEachChild(node, countComplexity);
55
+
56
+ if (complexity > maxComplexity) {
57
+ failures.push({
58
+ id: 'AST_COMPLEXITY',
59
+ title: `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
60
+ details: `High complexity detected in ${relativePath}`,
61
+ files: [relativePath],
62
+ hint: `Refactor '${name}' into smaller, more focused functions.`
63
+ });
64
+ }
65
+ }
66
+
67
+ if (ts.isClassDeclaration(node)) {
68
+ const name = node.name?.text || 'Anonymous Class';
69
+ const methods = node.members.filter(ts.isMethodDeclaration);
70
+
71
+ if (methods.length > maxMethods) {
72
+ failures.push({
73
+ id: 'AST_MAX_METHODS',
74
+ title: `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
75
+ details: `God Object pattern detected in ${relativePath}`,
76
+ files: [relativePath],
77
+ hint: `Class '${name}' is becoming too large. Split it into smaller services.`
78
+ });
79
+ }
80
+ }
81
+
82
+ if (ts.isImportDeclaration(node)) {
83
+ const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
84
+ this.checkBoundary(importPath, relativePath, failures);
85
+ }
86
+
87
+ ts.forEachChild(node, visit);
88
+ };
89
+
90
+ ts.forEachChild(sourceFile, visit);
91
+ }
92
+
93
+ private checkBoundary(importPath: string, relativePath: string, failures: Failure[]) {
94
+ const boundaries = (this.config as any).architecture?.boundaries || [];
95
+ for (const rule of boundaries) {
96
+ if (micromatch.isMatch(relativePath, rule.from)) {
97
+ const resolved = importPath.startsWith('.')
98
+ ? path.join(path.dirname(relativePath), importPath)
99
+ : importPath;
100
+
101
+ if (rule.mode === 'deny' && micromatch.isMatch(resolved, rule.to)) {
102
+ failures.push({
103
+ id: 'ARCH_BOUNDARY',
104
+ title: `Architectural Violation`,
105
+ details: `'${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`,
106
+ files: [relativePath],
107
+ hint: `Remove this import to maintain architectural layering.`
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ private getNodeName(node: ts.Node): string {
115
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
116
+ return node.name?.getText() || 'anonymous';
117
+ }
118
+ if (ts.isArrowFunction(node)) {
119
+ const parent = node.parent;
120
+ if (ts.isVariableDeclaration(parent)) return parent.name.getText();
121
+ return 'anonymous arrow';
122
+ }
123
+ return 'unknown';
124
+ }
125
+ }