hydra-nano 0.1.0 → 0.3.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.
@@ -0,0 +1,1122 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export const GURU_URL = 'https://refactoring.guru';
5
+
6
+ // Full Refactoring.Guru Catalog of Code Smells (22 smells in 5 families + 2 complexity smells)
7
+ export const SMELLS_CATALOG = {
8
+ 'long-method': {
9
+ name: 'Long Method',
10
+ family: 'Bloaters',
11
+ threshold: '>35 lines',
12
+ description: 'A method contains too many lines of code. Generally, any method longer than 10-15 lines should raise questions.',
13
+ refactorings: ['Extract Method', 'Replace Temp with Query', 'Introduce Parameter Object', 'Preserve Whole Object', 'Replace Method with Method Object'],
14
+ url: `${GURU_URL}/smells/long-method`,
15
+ },
16
+ 'large-class': {
17
+ name: 'Large Class',
18
+ family: 'Bloaters',
19
+ threshold: '>250 lines or >12 methods',
20
+ description: 'A class contains too many fields, methods, or lines of code, violating the Single Responsibility Principle.',
21
+ refactorings: ['Extract Class', 'Extract Subclass', 'Extract Interface', 'Duplicate Observed Data'],
22
+ url: `${GURU_URL}/smells/large-class`,
23
+ },
24
+ 'primitive-obsession': {
25
+ name: 'Primitive Obsession',
26
+ family: 'Bloaters',
27
+ threshold: 'Using primitives instead of small objects for simple tasks (currency, ranges, phone numbers)',
28
+ description: 'Use of primitives instead of small domain objects (such as ranges, currency, coordinates).',
29
+ refactorings: ['Replace Data Value with Object', 'Introduce Parameter Object', 'Replace Type Code with Class', 'Replace Type Code with Subclasses', 'Replace Type Code with State/Strategy'],
30
+ url: `${GURU_URL}/smells/primitive-obsession`,
31
+ },
32
+ 'long-parameter-list': {
33
+ name: 'Long Parameter List',
34
+ family: 'Bloaters',
35
+ threshold: '>4 parameters',
36
+ description: 'More than three or four parameters for a method make it hard to understand and prone to bugs.',
37
+ refactorings: ['Replace Parameter with Method Call', 'Preserve Whole Object', 'Introduce Parameter Object', 'Remove Setting Method'],
38
+ url: `${GURU_URL}/smells/long-parameter-list`,
39
+ },
40
+ 'data-clumps': {
41
+ name: 'Data Clumps',
42
+ family: 'Bloaters',
43
+ threshold: '>=3 identical parameters appearing together across functions',
44
+ description: 'Different parts of the code contain identical groups of variables (such as parameters for connecting to a database).',
45
+ refactorings: ['Extract Class', 'Introduce Parameter Object', 'Preserve Whole Object'],
46
+ url: `${GURU_URL}/smells/data-clumps`,
47
+ },
48
+ 'switch-statements': {
49
+ name: 'Switch Statements',
50
+ family: 'Object-Orientation Abusers',
51
+ threshold: 'Complex switch or chained if-else with >=4 branches',
52
+ description: 'Complex switch operator or sequence of if statements checking type codes or object states.',
53
+ refactorings: ['Replace Conditional with Polymorphism', 'Replace Type Code with Subclasses', 'Replace Type Code with State/Strategy', 'Replace Parameter with Explicit Methods', 'Introduce Null Object'],
54
+ url: `${GURU_URL}/smells/switch-statements`,
55
+ },
56
+ 'temporary-field': {
57
+ name: 'Temporary Field',
58
+ family: 'Object-Orientation Abusers',
59
+ threshold: 'Fields only set/used under specific circumstances',
60
+ description: 'Temporary fields get values (and so are needed by objects) only under certain circumstances.',
61
+ refactorings: ['Extract Class', 'Replace Method with Method Object', 'Introduce Null Object'],
62
+ url: `${GURU_URL}/smells/temporary-field`,
63
+ },
64
+ 'refused-bequest': {
65
+ name: 'Refused Bequest',
66
+ family: 'Object-Orientation Abusers',
67
+ threshold: 'Subclass inherits methods it ignores or throws unsupported exceptions on',
68
+ description: 'If a subclass uses only some of the methods and properties inherited from its parents.',
69
+ refactorings: ['Replace Inheritance with Delegation', 'Extract Superclass'],
70
+ url: `${GURU_URL}/smells/refused-bequest`,
71
+ },
72
+ 'alternative-classes': {
73
+ name: 'Alternative Classes with Different Interfaces',
74
+ family: 'Object-Orientation Abusers',
75
+ threshold: 'Classes performing identical tasks with different method signatures',
76
+ description: 'Two classes perform identical functions but have different method names.',
77
+ refactorings: ['Rename Method', 'Move Method', 'Extract Superclass'],
78
+ url: `${GURU_URL}/smells/alternative-classes-with-different-interfaces`,
79
+ },
80
+ 'divergent-change': {
81
+ name: 'Divergent Change',
82
+ family: 'Change Preventers',
83
+ threshold: 'One class modified for multiple unrelated requirements (SRP violation)',
84
+ description: 'You find yourself having to change many unrelated methods when you make changes to a class.',
85
+ refactorings: ['Extract Class', 'Extract Subclass', 'Extract Superclass'],
86
+ url: `${GURU_URL}/smells/divergent-change`,
87
+ },
88
+ 'shotgun-surgery': {
89
+ name: 'Shotgun Surgery',
90
+ family: 'Change Preventers',
91
+ threshold: 'A single change forces edits across multiple classes/files',
92
+ description: 'Making any modifications requires that you make many small changes to many different classes.',
93
+ refactorings: ['Move Method', 'Move Field', 'Inline Class'],
94
+ url: `${GURU_URL}/smells/shotgun-surgery`,
95
+ },
96
+ 'parallel-inheritance': {
97
+ name: 'Parallel Inheritance Hierarchies',
98
+ family: 'Change Preventers',
99
+ threshold: 'Adding a subclass to one hierarchy requires adding a subclass to another',
100
+ description: 'Whenever you create a subclass for a class, you find yourself needing to create a subclass for another class.',
101
+ refactorings: ['Move Method', 'Move Field'],
102
+ url: `${GURU_URL}/smells/parallel-inheritance-hierarchies`,
103
+ },
104
+ 'comments': {
105
+ name: 'Comments as Deodorant',
106
+ family: 'Dispensables',
107
+ threshold: 'Explanatory comments placed above large, cryptic code blocks',
108
+ description: 'A method is filled with explanatory comments because the code itself is unclear.',
109
+ refactorings: ['Extract Variable', 'Extract Method', 'Rename Method', 'Introduce Assertion'],
110
+ url: `${GURU_URL}/smells/comments`,
111
+ },
112
+ 'duplicate-code': {
113
+ name: 'Duplicate Code',
114
+ family: 'Dispensables',
115
+ threshold: '>=4 identical non-trivial lines repeated in code',
116
+ description: 'Two code fragments look almost identical, violating the DRY (Don\'t Repeat Yourself) principle.',
117
+ refactorings: ['Extract Method', 'Pull Up Method', 'Form Template Method', 'Substitute Algorithm', 'Extract Class'],
118
+ url: `${GURU_URL}/smells/duplicate-code`,
119
+ },
120
+ 'lazy-class': {
121
+ name: 'Lazy Class',
122
+ family: 'Dispensables',
123
+ threshold: 'Class doing almost nothing',
124
+ description: 'A class that does not do enough to earn its keep should be eliminated.',
125
+ refactorings: ['Inline Class', 'Collapse Hierarchy'],
126
+ url: `${GURU_URL}/smells/lazy-class`,
127
+ },
128
+ 'data-class': {
129
+ name: 'Data Class',
130
+ family: 'Dispensables',
131
+ threshold: 'Class with only fields/getters/setters and zero behavior',
132
+ description: 'A class that refers only to fields and crude methods for accessing them (getters and setters).',
133
+ refactorings: ['Move Method', 'Encapsulate Field', 'Encapsulate Collection', 'Remove Setting Method'],
134
+ url: `${GURU_URL}/smells/data-class`,
135
+ },
136
+ 'dead-code': {
137
+ name: 'Dead Code',
138
+ family: 'Dispensables',
139
+ threshold: 'Unreachable code following return/throw/break or unused functions',
140
+ description: 'A variable, parameter, field, method or class is no longer used (usually because it is obsolete).',
141
+ refactorings: ['Inline Class', 'Collapse Hierarchy', 'Remove Parameter', 'Remove Dead Code'],
142
+ url: `${GURU_URL}/smells/dead-code`,
143
+ },
144
+ 'speculative-generality': {
145
+ name: 'Speculative Generality',
146
+ family: 'Dispensables',
147
+ threshold: 'Unused abstract classes, interfaces or unused hooks created for "future use"',
148
+ description: 'There is an unused class, method, field or parameter created to support anticipated future features.',
149
+ refactorings: ['Collapse Hierarchy', 'Inline Class', 'Inline Method', 'Remove Parameter'],
150
+ url: `${GURU_URL}/smells/speculative-generality`,
151
+ },
152
+ 'feature-envy': {
153
+ name: 'Feature Envy',
154
+ family: 'Couplers',
155
+ threshold: 'A method accesses the data of another object more than its own',
156
+ description: 'A method accesses the data of another object more than its own data.',
157
+ refactorings: ['Move Method', 'Extract Method'],
158
+ url: `${GURU_URL}/smells/feature-envy`,
159
+ },
160
+ 'inappropriate-intimacy': {
161
+ name: 'Inappropriate Intimacy',
162
+ family: 'Couplers',
163
+ threshold: 'One class uses the internal fields and methods of another',
164
+ description: 'One class uses the internal fields and methods of another class, violating encapsulation.',
165
+ refactorings: ['Move Method', 'Move Field', 'Change Bidirectional Association to Unidirectional', 'Hide Delegate'],
166
+ url: `${GURU_URL}/smells/inappropriate-intimacy`,
167
+ },
168
+ 'message-chains': {
169
+ name: 'Message Chains',
170
+ family: 'Couplers',
171
+ threshold: '>=3 consecutive method/property invocations (a.b().c().d())',
172
+ description: 'In code you see a series of calls resembling $a->b()->c()->d(), violating the Law of Demeter.',
173
+ refactorings: ['Hide Delegate', 'Extract Method', 'Move Method'],
174
+ url: `${GURU_URL}/smells/message-chains`,
175
+ },
176
+ 'middle-man': {
177
+ name: 'Middle Man',
178
+ family: 'Couplers',
179
+ threshold: 'Class where most methods simply delegate to another class',
180
+ description: 'If a class performs only one action, that of delegating work to another class.',
181
+ refactorings: ['Remove Middle Man', 'Inline Method', 'Replace Delegation with Inheritance'],
182
+ url: `${GURU_URL}/smells/middle-man`,
183
+ },
184
+ 'incomplete-library-class': {
185
+ name: 'Incomplete Library Class',
186
+ family: 'Couplers',
187
+ threshold: 'Third-party library class lacking a required feature',
188
+ description: 'A vendor library stops meeting user needs and you cannot change its source.',
189
+ refactorings: ['Introduce Foreign Method', 'Introduce Local Extension'],
190
+ url: `${GURU_URL}/smells/incomplete-library-class`,
191
+ },
192
+ 'deep-nesting': {
193
+ name: 'Deep Nesting',
194
+ family: 'Complexity',
195
+ threshold: 'Indentation depth >=4 levels',
196
+ description: 'Deeply nested control structures (if/for/while/try) severely impede readability and maintainability.',
197
+ refactorings: ['Replace Nested Conditional with Guard Clauses', 'Decompose Conditional', 'Extract Method'],
198
+ url: `${GURU_URL}/replace-nested-conditional-with-guard-clauses`,
199
+ },
200
+ 'magic-number': {
201
+ name: 'Magic Number',
202
+ family: 'Organizing Data',
203
+ threshold: 'Unexplained numeric literals in code logic',
204
+ description: 'A numeric literal in code whose meaning is not obvious from context.',
205
+ refactorings: ['Replace Magic Number with Symbolic Constant'],
206
+ url: `${GURU_URL}/replace-magic-number-with-symbolic-constant`,
207
+ },
208
+ };
209
+
210
+ // Full Refactoring.Guru Catalog of Design Patterns (All 22 GoF patterns across 3 categories)
211
+ export const PATTERNS_CATALOG = {
212
+ // Creational (5)
213
+ 'factory-method': {
214
+ name: 'Factory Method',
215
+ category: 'Creational',
216
+ intent: 'Defines an interface for creating an object, but lets subclasses decide which class to instantiate.',
217
+ applicability: 'Use when you don\'t know beforehand the exact types and dependencies of the objects your code should work with, or when you want to provide users with a way to extend library components.',
218
+ smellsAddressed: ['switch-statements', 'primitive-obsession'],
219
+ url: `${GURU_URL}/design-patterns/factory-method`,
220
+ },
221
+ 'abstract-factory': {
222
+ name: 'Abstract Factory',
223
+ category: 'Creational',
224
+ intent: 'Lets you produce families of related or dependent objects without specifying their concrete classes.',
225
+ applicability: 'Use when your code needs to work with various families of related products, but you don\'t want it to depend on the concrete classes of those products.',
226
+ smellsAddressed: ['divergent-change', 'switch-statements'],
227
+ url: `${GURU_URL}/design-patterns/abstract-factory`,
228
+ },
229
+ 'builder': {
230
+ name: 'Builder',
231
+ category: 'Creational',
232
+ intent: 'Lets you construct complex objects step by step, producing different types and representations of an object using the same construction code.',
233
+ applicability: 'Use to get rid of a "telescoping constructor" with 5+ parameters, or when construction involves complex multi-step processes.',
234
+ smellsAddressed: ['long-parameter-list', 'large-class', 'primitive-obsession'],
235
+ url: `${GURU_URL}/design-patterns/builder`,
236
+ },
237
+ 'prototype': {
238
+ name: 'Prototype',
239
+ category: 'Creational',
240
+ intent: 'Lets you copy existing objects without making your code dependent on their classes.',
241
+ applicability: 'Use when your code shouldn\'t depend on the concrete classes of objects that you need to copy, or to reduce subclassing for object creation.',
242
+ smellsAddressed: ['duplicate-code'],
243
+ url: `${GURU_URL}/design-patterns/prototype`,
244
+ },
245
+ 'singleton': {
246
+ name: 'Singleton',
247
+ category: 'Creational',
248
+ intent: 'Lets you ensure that a class has only one instance, while providing a global access point to this instance.',
249
+ applicability: 'Use when a class in your program should have just a single instance available to all clients (e.g. single database connection, configuration registry).',
250
+ smellsAddressed: [],
251
+ url: `${GURU_URL}/design-patterns/singleton`,
252
+ },
253
+ // Structural (7)
254
+ 'adapter': {
255
+ name: 'Adapter',
256
+ category: 'Structural',
257
+ intent: 'Allows objects with incompatible interfaces to collaborate.',
258
+ applicability: 'Use when you want to use some existing class, but its interface isn\'t compatible with the rest of your code, or when integrating 3rd party libraries.',
259
+ smellsAddressed: ['alternative-classes', 'incomplete-library-class'],
260
+ url: `${GURU_URL}/design-patterns/adapter`,
261
+ },
262
+ 'bridge': {
263
+ name: 'Bridge',
264
+ category: 'Structural',
265
+ intent: 'Lets you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently.',
266
+ applicability: 'Use when you want to divide and organize a monolithic class that has several variants of some functionality (such as cross-platform GUI/renderers).',
267
+ smellsAddressed: ['parallel-inheritance', 'large-class'],
268
+ url: `${GURU_URL}/design-patterns/bridge`,
269
+ },
270
+ 'composite': {
271
+ name: 'Composite',
272
+ category: 'Structural',
273
+ intent: 'Lets you compose objects into tree structures and then work with these structures as if they were individual objects.',
274
+ applicability: 'Use when you need to implement a tree-like object structure (such as nested document nodes, UI widgets, or file hierarchies).',
275
+ smellsAddressed: ['switch-statements', 'duplicate-code'],
276
+ url: `${GURU_URL}/design-patterns/composite`,
277
+ },
278
+ 'decorator': {
279
+ name: 'Decorator',
280
+ category: 'Structural',
281
+ intent: 'Lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.',
282
+ applicability: 'Use when you need to be able to assign extra behaviors to objects at runtime without breaking the code that uses these objects, or when extending by inheritance is awkward.',
283
+ smellsAddressed: ['large-class', 'parallel-inheritance'],
284
+ url: `${GURU_URL}/design-patterns/decorator`,
285
+ },
286
+ 'facade': {
287
+ name: 'Facade',
288
+ category: 'Structural',
289
+ intent: 'Provides a simplified interface to a library, a framework, or any other complex set of classes.',
290
+ applicability: 'Use when you need to have a limited but straightforward interface to a complex subsystem with dozens of interrelated classes.',
291
+ smellsAddressed: ['shotgun-surgery', 'inappropriate-intimacy'],
292
+ url: `${GURU_URL}/design-patterns/facade`,
293
+ },
294
+ 'flyweight': {
295
+ name: 'Flyweight',
296
+ category: 'Structural',
297
+ intent: 'Lets you fit more objects into the available amount of RAM by sharing common parts of state between multiple objects instead of keeping all of the data in each object.',
298
+ applicability: 'Use only when your program must spawn a huge number of similar objects which barely fit into available RAM.',
299
+ smellsAddressed: ['large-class'],
300
+ url: `${GURU_URL}/design-patterns/flyweight`,
301
+ },
302
+ 'proxy': {
303
+ name: 'Proxy',
304
+ category: 'Structural',
305
+ intent: 'Lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request reaches the original object.',
306
+ applicability: 'Use for lazy initialization (virtual proxy), access control (protection proxy), local execution of a remote service (remote proxy), or logging/caching.',
307
+ smellsAddressed: ['middle-man'],
308
+ url: `${GURU_URL}/design-patterns/proxy`,
309
+ },
310
+ // Behavioral (10)
311
+ 'chain-of-responsibility': {
312
+ name: 'Chain of Responsibility',
313
+ category: 'Behavioral',
314
+ intent: 'Lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.',
315
+ applicability: 'Use when your program is expected to process different kinds of requests in various ways, but the exact types of requests and their sequences are unknown beforehand (middleware, filters).',
316
+ smellsAddressed: ['deep-nesting', 'switch-statements', 'long-method'],
317
+ url: `${GURU_URL}/design-patterns/chain-of-responsibility`,
318
+ },
319
+ 'command': {
320
+ name: 'Command',
321
+ category: 'Behavioral',
322
+ intent: 'Turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as method arguments, delay or queue a request\'s execution, and support undoable operations.',
323
+ applicability: 'Use when you want to parameterize objects with operations, queue operations, schedule their execution, or support undo/redo.',
324
+ smellsAddressed: ['long-method', 'large-class'],
325
+ url: `${GURU_URL}/design-patterns/command`,
326
+ },
327
+ 'iterator': {
328
+ name: 'Iterator',
329
+ category: 'Behavioral',
330
+ intent: 'Lets you traverse elements of a collection without exposing its underlying representation (list, stack, tree, etc.).',
331
+ applicability: 'Use when your collection has a complex data structure under the hood, but you want to hide its complexity from clients.',
332
+ smellsAddressed: ['feature-envy'],
333
+ url: `${GURU_URL}/design-patterns/iterator`,
334
+ },
335
+ 'mediator': {
336
+ name: 'Mediator',
337
+ category: 'Behavioral',
338
+ intent: 'Lets you reduce chaotic dependencies between objects. The pattern restricts direct communications between the objects and forces them to collaborate only via a mediator object.',
339
+ applicability: 'Use when it\'s hard to change some of the classes because they are tightly coupled to a dozen of other classes.',
340
+ smellsAddressed: ['inappropriate-intimacy', 'shotgun-surgery'],
341
+ url: `${GURU_URL}/design-patterns/mediator`,
342
+ },
343
+ 'memento': {
344
+ name: 'Memento',
345
+ category: 'Behavioral',
346
+ intent: 'Lets you save and restore the previous state of an object without revealing the details of its implementation.',
347
+ applicability: 'Use when you want to produce snapshots of the object\'s state to be able to restore a previous state of the object (e.g. undo, transactions).',
348
+ smellsAddressed: ['inappropriate-intimacy'],
349
+ url: `${GURU_URL}/design-patterns/memento`,
350
+ },
351
+ 'observer': {
352
+ name: 'Observer',
353
+ category: 'Behavioral',
354
+ intent: 'Lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they\'re observing.',
355
+ applicability: 'Use when changes to the state of one object may require changing other objects, and the actual set of objects is unknown beforehand or changes dynamically.',
356
+ smellsAddressed: ['divergent-change', 'shotgun-surgery'],
357
+ url: `${GURU_URL}/design-patterns/observer`,
358
+ },
359
+ 'state': {
360
+ name: 'State',
361
+ category: 'Behavioral',
362
+ intent: 'Lets an object alter its behavior when its internal state changes. It appears as if the object changed its class.',
363
+ applicability: 'Use when you have an object that behaves differently depending on its current state, the number of states is enormous, and the state-specific code changes frequently.',
364
+ smellsAddressed: ['switch-statements', 'temporary-field'],
365
+ url: `${GURU_URL}/design-patterns/state`,
366
+ },
367
+ 'strategy': {
368
+ name: 'Strategy',
369
+ category: 'Behavioral',
370
+ intent: 'Lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.',
371
+ applicability: 'Use when you want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime.',
372
+ smellsAddressed: ['switch-statements', 'long-method', 'duplicate-code'],
373
+ url: `${GURU_URL}/design-patterns/strategy`,
374
+ },
375
+ 'template-method': {
376
+ name: 'Template Method',
377
+ category: 'Behavioral',
378
+ intent: 'Defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.',
379
+ applicability: 'Use when you want to let clients extend only particular steps of an algorithm, but not the whole algorithm or its structure.',
380
+ smellsAddressed: ['duplicate-code'],
381
+ url: `${GURU_URL}/design-patterns/template-method`,
382
+ },
383
+ 'visitor': {
384
+ name: 'Visitor',
385
+ category: 'Behavioral',
386
+ intent: 'Lets you separate algorithms from the objects on which they operate.',
387
+ applicability: 'Use when you need to perform an operation on all elements of a complex object structure (e.g. an AST tree or object graph).',
388
+ smellsAddressed: ['divergent-change'],
389
+ url: `${GURU_URL}/design-patterns/visitor`,
390
+ },
391
+ };
392
+
393
+ // Patterns for functions, methods and classes across JS, TS, Python, Go, Rust, Java
394
+ const FUNCTION_PATTERNS = [
395
+ /^(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s*([a-zA-Z0-9_$]+)\s*\(([^)]*)\)/,
396
+ /^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
397
+ /^(?:async\s+)?def\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/,
398
+ /^(?:pub(?:\([^)]*\))?\s+)?(?:(?:const|async|unsafe|extern(?:\s+"[^"]*")?)\s+)*fn\s+([a-zA-Z0-9_]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)/,
399
+ /^func\s+(?:\([^)]*\)\s+)?([a-zA-Z0-9_]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)/,
400
+ /^\s*(?:public|private|protected|static|final|native|synchronized|abstract|\s)*\s+[a-zA-Z0-9_<>[\], ]+\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)\s*(?:throws\s+[a-zA-Z0-9_, ]+)?\s*\{/,
401
+ ];
402
+
403
+ const CLASS_PATTERNS = [
404
+ /^(?:export\s+(?:default\s+)?)?class\s+([a-zA-Z0-9_$]+)/,
405
+ /^class\s+([a-zA-Z0-9_]+)\s*(?:\([^)]*\))?:/,
406
+ /^(?:pub(?:\([^)]*\))?\s+)?struct\s+([a-zA-Z0-9_]+)/,
407
+ /^type\s+([a-zA-Z0-9_]+)\s+struct/,
408
+ ];
409
+
410
+ /**
411
+ * Scan source code string for Refactoring.Guru code smells.
412
+ */
413
+ export function scanCode(content, filename = 'code.ts', options = {}) {
414
+ const maxMethodLines = options.maxMethodLines ?? 35;
415
+ const maxClassLines = options.maxClassLines ?? 250;
416
+ const maxParams = options.maxParams ?? 4;
417
+ const maxNesting = options.maxNesting ?? 3;
418
+
419
+ const lines = content.split('\n');
420
+ const findings = [];
421
+ const paramClumps = new Map();
422
+
423
+ // 1. Structure discovery: locate functions, classes
424
+ const functions = [];
425
+ const classes = [];
426
+
427
+ for (let i = 0; i < lines.length; i++) {
428
+ const line = lines[i];
429
+ const trimmed = line.trim();
430
+
431
+ for (const pat of CLASS_PATTERNS) {
432
+ const m = pat.exec(trimmed);
433
+ if (m) {
434
+ classes.push({ name: m[1], startLine: i + 1, indent: line.search(/\S/) });
435
+ break;
436
+ }
437
+ }
438
+
439
+ for (const pat of FUNCTION_PATTERNS) {
440
+ const m = pat.exec(trimmed);
441
+ if (m) {
442
+ const name = m[1];
443
+ const paramsRaw = m[2] ? m[2].trim() : '';
444
+ const params = paramsRaw
445
+ ? paramsRaw.split(',').map(p => p.trim().split(/[\s:=]/)[0].replace(/^[*&]/, '')).filter(Boolean)
446
+ : [];
447
+ functions.push({ name, params, startLine: i + 1, indent: line.search(/\S/) });
448
+ break;
449
+ }
450
+ }
451
+ }
452
+
453
+ // Calculate function boundaries and analyze them
454
+ for (let idx = 0; idx < functions.length; idx++) {
455
+ const fn = functions[idx];
456
+ const nextFn = functions[idx + 1];
457
+ let endLine = lines.length;
458
+ if (nextFn && nextFn.indent <= fn.indent) {
459
+ endLine = nextFn.startLine - 1;
460
+ } else {
461
+ for (let j = fn.startLine; j < lines.length; j++) {
462
+ if (/^\s*[}\]]\s*;?\s*$/.test(lines[j]) && lines[j].search(/\S/) <= fn.indent) {
463
+ endLine = j + 1;
464
+ break;
465
+ }
466
+ }
467
+ }
468
+ fn.endLine = Math.min(lines.length, Math.max(fn.startLine, endLine));
469
+ fn.lineCount = fn.endLine - fn.startLine + 1;
470
+
471
+ // Smell: Long Method
472
+ if (fn.lineCount > maxMethodLines) {
473
+ findings.push({
474
+ smell: 'long-method',
475
+ name: SMELLS_CATALOG['long-method'].name,
476
+ family: SMELLS_CATALOG['long-method'].family,
477
+ symbol: fn.name,
478
+ line: fn.startLine,
479
+ endLine: fn.endLine,
480
+ metric: `${fn.lineCount} lines (limit: ${maxMethodLines})`,
481
+ recipe: 'Extract Method: break into smaller focused helper functions.',
482
+ url: SMELLS_CATALOG['long-method'].url,
483
+ });
484
+ }
485
+
486
+ // Smell: Long Parameter List
487
+ if (fn.params.length > maxParams) {
488
+ findings.push({
489
+ smell: 'long-parameter-list',
490
+ name: SMELLS_CATALOG['long-parameter-list'].name,
491
+ family: SMELLS_CATALOG['long-parameter-list'].family,
492
+ symbol: fn.name,
493
+ line: fn.startLine,
494
+ metric: `${fn.params.length} parameters (${fn.params.join(', ')})`,
495
+ recipe: 'Introduce Parameter Object: group related parameters into a dedicated options object/struct.',
496
+ url: SMELLS_CATALOG['long-parameter-list'].url,
497
+ });
498
+ }
499
+
500
+ // Record parameter clump candidate (if >= 3 params)
501
+ if (fn.params.length >= 3) {
502
+ const key = fn.params.slice().sort().join(',');
503
+ if (!paramClumps.has(key)) paramClumps.set(key, []);
504
+ paramClumps.get(key).push({ fnName: fn.name, line: fn.startLine, params: fn.params });
505
+ }
506
+ }
507
+
508
+ // Calculate class boundaries
509
+ for (let idx = 0; idx < classes.length; idx++) {
510
+ const cls = classes[idx];
511
+ const nextCls = classes[idx + 1];
512
+ let endLine = nextCls ? nextCls.startLine - 1 : lines.length;
513
+ for (let j = cls.startLine; j < lines.length; j++) {
514
+ if (/^\s*[}\]]\s*;?\s*$/.test(lines[j]) && lines[j].search(/\S/) <= cls.indent) {
515
+ endLine = j + 1;
516
+ break;
517
+ }
518
+ }
519
+ cls.endLine = Math.min(lines.length, Math.max(cls.startLine, endLine));
520
+ cls.lineCount = cls.endLine - cls.startLine + 1;
521
+
522
+ const methods = functions.filter(f => f.startLine >= cls.startLine && f.endLine <= cls.endLine);
523
+ if (cls.lineCount > maxClassLines || methods.length > 12) {
524
+ findings.push({
525
+ smell: 'large-class',
526
+ name: SMELLS_CATALOG['large-class'].name,
527
+ family: SMELLS_CATALOG['large-class'].family,
528
+ symbol: cls.name,
529
+ line: cls.startLine,
530
+ endLine: cls.endLine,
531
+ metric: `${cls.lineCount} lines, ${methods.length} methods (limit: ${maxClassLines} lines)`,
532
+ recipe: 'Extract Class: partition responsibilities and move cohesive methods to a new class.',
533
+ url: SMELLS_CATALOG['large-class'].url,
534
+ });
535
+ }
536
+ }
537
+
538
+ // Smell: Data Clumps (same >=3 parameters appearing in multiple functions)
539
+ for (const [key, occurrences] of paramClumps.entries()) {
540
+ if (occurrences.length >= 2) {
541
+ const sample = occurrences[0];
542
+ findings.push({
543
+ smell: 'data-clumps',
544
+ name: SMELLS_CATALOG['data-clumps'].name,
545
+ family: SMELLS_CATALOG['data-clumps'].family,
546
+ symbol: occurrences.map(o => o.fnName).join(', '),
547
+ line: sample.line,
548
+ metric: `Identical parameter cluster [${sample.params.join(', ')}] across ${occurrences.length} functions`,
549
+ recipe: 'Extract Class / Introduce Parameter Object: wrap clumped fields into a unified class or record.',
550
+ url: SMELLS_CATALOG['data-clumps'].url,
551
+ });
552
+ }
553
+ }
554
+
555
+ // 2. Line-by-line inspection (Nesting, Switch Statements, Dead Code, Magic Numbers, Message Chains)
556
+ let switchCaseCount = 0;
557
+ let switchStartLine = -1;
558
+ let nestingStart = -1;
559
+ let maxDepthInBlock = 0;
560
+
561
+ for (let i = 0; i < lines.length; i++) {
562
+ const line = lines[i];
563
+ const trimmed = line.trim();
564
+ const lineNum = i + 1;
565
+
566
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('/*')) {
567
+ if (nestingStart !== -1) {
568
+ const endLine = lineNum - 1;
569
+ findings.push({
570
+ smell: 'deep-nesting',
571
+ name: SMELLS_CATALOG['deep-nesting'].name,
572
+ family: SMELLS_CATALOG['deep-nesting'].family,
573
+ line: nestingStart,
574
+ endLine: endLine > nestingStart ? endLine : undefined,
575
+ metric: `Indentation depth ${maxDepthInBlock} across ${endLine - nestingStart + 1} lines (limit: ${maxNesting})`,
576
+ recipe: 'Replace Nested Conditional with Guard Clauses: return early or decompose sub-conditions.',
577
+ url: SMELLS_CATALOG['deep-nesting'].url,
578
+ });
579
+ nestingStart = -1;
580
+ }
581
+ continue;
582
+ }
583
+
584
+ // Indentation depth check (supports 2-space, 4-space and tabs)
585
+ const leadingMatch = line.match(/^([ \t]*)/);
586
+ const leading = leadingMatch ? leadingMatch[1] : '';
587
+ let spaceCount = 0;
588
+ let tabCount = 0;
589
+ for (const ch of leading) {
590
+ if (ch === '\t') tabCount++;
591
+ else spaceCount++;
592
+ }
593
+ const depthFromSpaces = Math.floor(spaceCount / 2);
594
+ const depth = tabCount + depthFromSpaces;
595
+
596
+ if (depth > maxNesting && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
597
+ if (nestingStart === -1) {
598
+ nestingStart = lineNum;
599
+ maxDepthInBlock = depth;
600
+ } else {
601
+ maxDepthInBlock = Math.max(maxDepthInBlock, depth);
602
+ }
603
+ } else {
604
+ if (nestingStart !== -1) {
605
+ const endLine = lineNum - 1;
606
+ findings.push({
607
+ smell: 'deep-nesting',
608
+ name: SMELLS_CATALOG['deep-nesting'].name,
609
+ family: SMELLS_CATALOG['deep-nesting'].family,
610
+ line: nestingStart,
611
+ endLine: endLine > nestingStart ? endLine : undefined,
612
+ metric: `Indentation depth ${maxDepthInBlock} across ${endLine - nestingStart + 1} lines (limit: ${maxNesting})`,
613
+ recipe: 'Replace Nested Conditional with Guard Clauses: return early or decompose sub-conditions.',
614
+ url: SMELLS_CATALOG['deep-nesting'].url,
615
+ });
616
+ nestingStart = -1;
617
+ }
618
+ }
619
+
620
+ // Switch statements & chained if-else
621
+ if (/^\s*switch\s*\(/.test(line)) {
622
+ switchStartLine = lineNum;
623
+ switchCaseCount = 0;
624
+ } else if (switchStartLine > 0) {
625
+ if (/^\s*(?:case\s+[^:]+|default)\s*:/.test(line)) {
626
+ switchCaseCount++;
627
+ }
628
+ if (/^\s*\}\s*$/.test(line)) {
629
+ if (switchCaseCount >= 4) {
630
+ findings.push({
631
+ smell: 'switch-statements',
632
+ name: SMELLS_CATALOG['switch-statements'].name,
633
+ family: SMELLS_CATALOG['switch-statements'].family,
634
+ line: switchStartLine,
635
+ endLine: lineNum,
636
+ metric: `Switch with ${switchCaseCount} branches`,
637
+ recipe: 'Replace Conditional with Polymorphism / Strategy Pattern: delegate execution to polymorphic classes.',
638
+ url: SMELLS_CATALOG['switch-statements'].url,
639
+ });
640
+ }
641
+ switchStartLine = -1;
642
+ }
643
+ }
644
+
645
+ // Dead Code detection: single-line terminal statements not opening a multiline block
646
+ if (/^\s*(?:return\b[^;{([,?:+\-=>]*;|throw\s+new\b[^;{([,?:+\-=>]*;|break;|continue;)\s*$/.test(trimmed)) {
647
+ const nextLine = lines[i + 1]?.trim();
648
+ if (nextLine && !/^[}\])]/.test(nextLine) && !/^(?:case\b|default\b|\/\/|\/\*)/.test(nextLine)) {
649
+ findings.push({
650
+ smell: 'dead-code',
651
+ name: SMELLS_CATALOG['dead-code'].name,
652
+ family: SMELLS_CATALOG['dead-code'].family,
653
+ line: lineNum + 1,
654
+ metric: `Unreachable code immediately following "${trimmed.slice(0, 30)}"`,
655
+ recipe: 'Remove Dead Code: delete unreachable statements.',
656
+ url: SMELLS_CATALOG['dead-code'].url,
657
+ });
658
+ }
659
+ }
660
+
661
+ // Message Chains: >= 3 method or property hops (e.g., a.b.c.d() or obj.getA().getB().getC())
662
+ if (/[a-zA-Z0-9_$]+\.[a-zA-Z0-9_$]+(?:\([^)]*\))?\.[a-zA-Z0-9_$]+(?:\([^)]*\))?\.[a-zA-Z0-9_$]+/.test(trimmed)) {
663
+ if (!trimmed.startsWith('import ') && !trimmed.startsWith('export ') && !trimmed.includes('console.log')) {
664
+ findings.push({
665
+ smell: 'message-chains',
666
+ name: SMELLS_CATALOG['message-chains'].name,
667
+ family: SMELLS_CATALOG['message-chains'].family,
668
+ line: lineNum,
669
+ metric: `Long invocation chain: "${trimmed.slice(0, 60)}"`,
670
+ recipe: 'Hide Delegate: encapsulate deep navigation behind a direct method on the immediate neighbor.',
671
+ url: SMELLS_CATALOG['message-chains'].url,
672
+ });
673
+ }
674
+ }
675
+
676
+ // Magic Numbers (unexplained raw literals > 2 that are not in constants/configs)
677
+ const magicMatch = /(?:[=!<>+\-*/,%&|]\s*|\b(?:if|while|for|return)\b[^(]*\([^)]*|\(\s*)\b([3-9]\d{1,5}|\d{3,6})\b/.exec(trimmed);
678
+ if (magicMatch && !trimmed.startsWith('const ') && !trimmed.startsWith('let ') && !/status|port|error|code|200|404|500/i.test(trimmed)) {
679
+ findings.push({
680
+ smell: 'magic-number',
681
+ name: SMELLS_CATALOG['magic-number'].name,
682
+ family: SMELLS_CATALOG['magic-number'].family,
683
+ line: lineNum,
684
+ metric: `Unexplained numeric literal: ${magicMatch[1]}`,
685
+ recipe: 'Replace Magic Number with Symbolic Constant: define a descriptive CONSTANT.',
686
+ url: SMELLS_CATALOG['magic-number'].url,
687
+ });
688
+ }
689
+ }
690
+
691
+ if (nestingStart !== -1) {
692
+ const endLine = lines.length;
693
+ findings.push({
694
+ smell: 'deep-nesting',
695
+ name: SMELLS_CATALOG['deep-nesting'].name,
696
+ family: SMELLS_CATALOG['deep-nesting'].family,
697
+ line: nestingStart,
698
+ endLine: endLine > nestingStart ? endLine : undefined,
699
+ metric: `Indentation depth ${maxDepthInBlock} across ${endLine - nestingStart + 1} lines (limit: ${maxNesting})`,
700
+ recipe: 'Replace Nested Conditional with Guard Clauses: return early or decompose sub-conditions.',
701
+ url: SMELLS_CATALOG['deep-nesting'].url,
702
+ });
703
+ }
704
+
705
+ // 3. Duplicate code detection (groups of >=4 contiguous non-trivial lines repeated)
706
+ const lineBlocks = new Map();
707
+ const BLOCK_SIZE = 4;
708
+ for (let i = 0; i <= lines.length - BLOCK_SIZE; i++) {
709
+ const slice = lines.slice(i, i + BLOCK_SIZE).map(l => l.trim()).filter(Boolean);
710
+ if (slice.length === BLOCK_SIZE && slice.every(l => l.length > 5 && !l.startsWith('//') && !l.startsWith('/*') && l !== '{' && l !== '}')) {
711
+ const key = slice.join(' ## ');
712
+ if (!lineBlocks.has(key)) lineBlocks.set(key, []);
713
+ lineBlocks.get(key).push(i + 1);
714
+ }
715
+ }
716
+
717
+ const reportedDuplicates = new Set();
718
+ for (const [key, occurrences] of lineBlocks.entries()) {
719
+ if (occurrences.length >= 2) {
720
+ const first = occurrences[0];
721
+ const second = occurrences[1];
722
+ if (second >= first + BLOCK_SIZE && !reportedDuplicates.has(first)) {
723
+ for (let b = 0; b < BLOCK_SIZE; b++) reportedDuplicates.add(first + b);
724
+ findings.push({
725
+ smell: 'duplicate-code',
726
+ name: SMELLS_CATALOG['duplicate-code'].name,
727
+ family: SMELLS_CATALOG['duplicate-code'].family,
728
+ line: first,
729
+ metric: `${BLOCK_SIZE}-line identical code block repeated at line ${second}`,
730
+ recipe: 'Extract Method: extract the duplicated code into a shared method.',
731
+ url: SMELLS_CATALOG['duplicate-code'].url,
732
+ });
733
+ }
734
+ }
735
+ }
736
+
737
+ // Sort findings by line number
738
+ findings.sort((a, b) => (a.line || 0) - (b.line || 0));
739
+
740
+ return {
741
+ filename,
742
+ totalLines: lines.length,
743
+ smellsFound: findings.length,
744
+ findings,
745
+ };
746
+ }
747
+
748
+ /**
749
+ * Scan a single file from disk.
750
+ */
751
+ export function scanFile(filePath, options = {}) {
752
+ const resolved = path.resolve(filePath);
753
+ if (!existsSync(resolved) || statSync(resolved).isDirectory()) {
754
+ throw new Error(`File not found: ${filePath}`);
755
+ }
756
+ const content = readFileSync(resolved, 'utf8');
757
+ return scanCode(content, filePath, options);
758
+ }
759
+
760
+ /**
761
+ * Scan a directory recursively (ignoring node_modules, .git, etc.).
762
+ */
763
+ export function scanDirectory(dirPath, options = {}) {
764
+ const resolved = path.resolve(dirPath);
765
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
766
+ throw new Error(`Directory not found: ${dirPath}`);
767
+ }
768
+
769
+ const SUPPORTED_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h', '.cs']);
770
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.hydra', '.next', 'target', 'vendor', '__pycache__']);
771
+
772
+ const reports = [];
773
+
774
+ function walk(current) {
775
+ const entries = readdirSync(current, { withFileTypes: true });
776
+ for (const entry of entries) {
777
+ if (entry.isDirectory()) {
778
+ if (!IGNORE_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
779
+ walk(path.join(current, entry.name));
780
+ }
781
+ } else if (entry.isFile()) {
782
+ const ext = path.extname(entry.name);
783
+ if (SUPPORTED_EXTS.has(ext)) {
784
+ const fullPath = path.join(current, entry.name);
785
+ try {
786
+ const rel = path.relative(resolved, fullPath);
787
+ const report = scanFile(fullPath, options);
788
+ if (report.smellsFound > 0) {
789
+ report.filename = rel;
790
+ reports.push(report);
791
+ }
792
+ } catch {
793
+ // Ignore unreadable files
794
+ }
795
+ }
796
+ }
797
+ }
798
+ }
799
+
800
+ walk(resolved);
801
+
802
+ const totalSmells = reports.reduce((sum, r) => sum + r.smellsFound, 0);
803
+
804
+ return {
805
+ directory: dirPath,
806
+ filesWithSmells: reports.length,
807
+ totalSmells,
808
+ reports,
809
+ };
810
+ }
811
+
812
+ /**
813
+ * Scan unified git diff (e.g. from `git diff` or `git diff --staged`) for introduced code smells.
814
+ */
815
+ export function scanDiff(diffText) {
816
+ const fileDiffs = [];
817
+ const lines = diffText.split('\n');
818
+ let currentFile = null;
819
+ let addedLines = [];
820
+ let currentLineNum = 0;
821
+
822
+ for (let i = 0; i < lines.length; i++) {
823
+ const line = lines[i];
824
+ if (line.startsWith('diff --git ')) {
825
+ if (currentFile && addedLines.length) {
826
+ fileDiffs.push({ file: currentFile, code: addedLines.map(l => l.text).join('\n') });
827
+ }
828
+ currentFile = line.split(' ')[2]?.replace(/^a\//, '') || 'unknown';
829
+ addedLines = [];
830
+ } else if (line.startsWith('@@ ')) {
831
+ const m = /\+([0-9]+)/.exec(line);
832
+ currentLineNum = m ? parseInt(m[1], 10) : 0;
833
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
834
+ addedLines.push({ num: currentLineNum, text: line.slice(1) });
835
+ currentLineNum++;
836
+ } else if (!line.startsWith('-')) {
837
+ currentLineNum++;
838
+ }
839
+ }
840
+
841
+ if (currentFile && addedLines.length) {
842
+ fileDiffs.push({ file: currentFile, code: addedLines.map(l => l.text).join('\n') });
843
+ }
844
+
845
+ const reports = [];
846
+ for (const { file, code } of fileDiffs) {
847
+ const report = scanCode(code, file);
848
+ if (report.smellsFound > 0) {
849
+ reports.push(report);
850
+ }
851
+ }
852
+
853
+ return {
854
+ totalFilesChecked: fileDiffs.length,
855
+ filesWithSmells: reports.length,
856
+ reports,
857
+ };
858
+ }
859
+
860
+ /**
861
+ * Generate a Refactoring.Guru step-by-step recipe for a specific symbol or line range.
862
+ */
863
+ export function generateRecipe(content, target = {}) {
864
+ const lines = content.split('\n');
865
+ let { symbol, startLine, endLine } = target;
866
+
867
+ if (symbol && (!startLine || !endLine)) {
868
+ const term = symbol.toLowerCase().trim();
869
+ for (let i = 0; i < lines.length; i++) {
870
+ for (const pat of [...FUNCTION_PATTERNS, ...CLASS_PATTERNS]) {
871
+ const m = pat.exec(lines[i].trim());
872
+ if (m && m[1].toLowerCase() === term) {
873
+ startLine = i + 1;
874
+ for (let j = i + 1; j < lines.length; j++) {
875
+ if (/^\s*[}\]]\s*;?\s*$/.test(lines[j]) && lines[j].search(/\S/) <= lines[i].search(/\S/)) {
876
+ endLine = j + 1;
877
+ break;
878
+ }
879
+ }
880
+ endLine = endLine || Math.min(lines.length, startLine + 40);
881
+ break;
882
+ }
883
+ }
884
+ if (startLine) break;
885
+ }
886
+ }
887
+
888
+ startLine = Math.max(1, startLine || 1);
889
+ endLine = Math.min(lines.length, endLine || lines.length);
890
+
891
+ const slice = lines.slice(startLine - 1, endLine).join('\n');
892
+ const scan = scanCode(slice, 'slice.ts');
893
+
894
+ const steps = [];
895
+
896
+ const hasNesting = scan.findings.some(f => f.smell === 'deep-nesting');
897
+ const hasLongMethod = scan.findings.some(f => f.smell === 'long-method') || (endLine - startLine > 30);
898
+ const hasSwitch = scan.findings.some(f => f.smell === 'switch-statements');
899
+ const hasLongParams = scan.findings.some(f => f.smell === 'long-parameter-list');
900
+ const hasMagicNumbers = scan.findings.some(f => f.smell === 'magic-number');
901
+
902
+ if (hasNesting) {
903
+ steps.push({
904
+ step: steps.length + 1,
905
+ technique: 'Replace Nested Conditional with Guard Clauses',
906
+ url: `${GURU_URL}/replace-nested-conditional-with-guard-clauses`,
907
+ goal: 'Flatten nested control structures by returning or continuing early on boundary/error conditions.',
908
+ instructions: [
909
+ 'Identify boundary conditions (null checks, empty lists, validation failures).',
910
+ 'Invert the nested "if (valid) { ...lots of code... }" into "if (!valid) return;".',
911
+ 'Place guard clauses at the very beginning of the function body.',
912
+ ],
913
+ });
914
+ }
915
+
916
+ if (hasLongParams) {
917
+ steps.push({
918
+ step: steps.length + 1,
919
+ technique: 'Introduce Parameter Object',
920
+ url: `${GURU_URL}/introduce-parameter-object`,
921
+ goal: 'Replace 4+ parameters with a single cohesive options record or domain object.',
922
+ instructions: [
923
+ 'Declare a new interface/type or class grouping these parameters.',
924
+ 'Update callers to pass an object literal containing these fields.',
925
+ 'Access properties via the parameter object inside the function body.',
926
+ ],
927
+ });
928
+ }
929
+
930
+ if (hasLongMethod) {
931
+ steps.push({
932
+ step: steps.length + 1,
933
+ technique: 'Extract Method',
934
+ url: `${GURU_URL}/extract-method`,
935
+ goal: 'Break long monolithic method into smaller, single-purpose helper functions.',
936
+ instructions: [
937
+ 'Group cohesive contiguous statements that perform one sub-task.',
938
+ 'Check which local variables are read and written by this chunk.',
939
+ 'Extract the chunk into a private/helper function named after what it does, not how it does it.',
940
+ 'Pass required local variables as arguments and return any modified value.',
941
+ ],
942
+ });
943
+ }
944
+
945
+ if (hasSwitch) {
946
+ steps.push({
947
+ step: steps.length + 1,
948
+ technique: 'Replace Conditional with Polymorphism / Strategy Pattern',
949
+ url: `${GURU_URL}/replace-conditional-with-polymorphism`,
950
+ goal: 'Eliminate fragile switch/if-else ladders by distributing branch behavior into polymorphic classes or handler strategies.',
951
+ instructions: [
952
+ 'Define a common interface or strategy function with the execution signature.',
953
+ 'Create a separate implementation/strategy class for each branch case.',
954
+ 'Use a dictionary/map lookup or factory to resolve the strategy at runtime.',
955
+ ],
956
+ });
957
+ }
958
+
959
+ if (hasMagicNumbers) {
960
+ steps.push({
961
+ step: steps.length + 1,
962
+ technique: 'Replace Magic Number with Symbolic Constant',
963
+ url: `${GURU_URL}/replace-magic-number-with-symbolic-constant`,
964
+ goal: 'Replace raw numbers with meaningful named constants.',
965
+ instructions: [
966
+ 'Declare a const at the top of the file/class describing what the number represents.',
967
+ 'Replace all occurrences of the literal number with the constant name.',
968
+ ],
969
+ });
970
+ }
971
+
972
+ if (steps.length === 0) {
973
+ steps.push({
974
+ step: 1,
975
+ technique: 'Clean Code Check',
976
+ url: `${GURU_URL}/refactoring/what-is-refactoring`,
977
+ goal: 'The selected code is relatively compact with no high-severity code smells detected.',
978
+ instructions: ['Verify test coverage before making architectural adjustments.'],
979
+ });
980
+ }
981
+
982
+ return {
983
+ symbol: symbol || `Lines ${startLine}-${endLine}`,
984
+ startLine,
985
+ endLine,
986
+ totalLines: endLine - startLine + 1,
987
+ smellsDetected: scan.findings.map(f => f.name),
988
+ steps,
989
+ };
990
+ }
991
+
992
+ /**
993
+ * Design Pattern Advisor: query by pattern name, smell, or architectural problem.
994
+ */
995
+ export function advisePattern(query = '', codeSnippet = '') {
996
+ const q = query.toLowerCase().trim();
997
+
998
+ // 1. Direct Pattern Lookup
999
+ for (const [id, pat] of Object.entries(PATTERNS_CATALOG)) {
1000
+ if (id === q || pat.name.toLowerCase() === q || id.replace(/-/g, '') === q.replace(/[\s-_]/g, '')) {
1001
+ return {
1002
+ matched: 'direct',
1003
+ pattern: pat.name,
1004
+ category: pat.category,
1005
+ intent: pat.intent,
1006
+ applicability: pat.applicability,
1007
+ smellsAddressed: pat.smellsAddressed.map(s => SMELLS_CATALOG[s]?.name || s),
1008
+ url: pat.url,
1009
+ };
1010
+ }
1011
+ }
1012
+
1013
+ // 2. Query Search across Intent, Applicability, and Smells
1014
+ const results = [];
1015
+ for (const [id, pat] of Object.entries(PATTERNS_CATALOG)) {
1016
+ let score = 0;
1017
+ if (q) {
1018
+ const words = q.split(/\s+/).filter(w => w.length >= 3);
1019
+ for (const w of words) {
1020
+ if (pat.name.toLowerCase().includes(w)) score += 5;
1021
+ if (pat.intent.toLowerCase().includes(w)) score += 3;
1022
+ if (pat.applicability.toLowerCase().includes(w)) score += 2;
1023
+ if (pat.smellsAddressed.some(s => s.includes(w))) score += 4;
1024
+ }
1025
+ }
1026
+ if (codeSnippet) {
1027
+ if (id === 'builder' && (codeSnippet.split(',').length > 5 || /constructor\s*\([^)]*,[^)]*,[^)]*,[^)]*,[^)]*\)/.test(codeSnippet))) score += 4;
1028
+ if (id === 'strategy' && (/switch\s*\(/.test(codeSnippet) || /if\s*\(.*===\s*['"][a-z]+['"]\)/i.test(codeSnippet))) score += 4;
1029
+ if (id === 'state' && /status|state|phase/i.test(codeSnippet) && /switch|case/i.test(codeSnippet)) score += 5;
1030
+ if (id === 'observer' && /subscribe|listener|emit|event|notify/i.test(codeSnippet)) score += 4;
1031
+ if (id === 'facade' && /import|require/.test(codeSnippet) && codeSnippet.split('\n').length > 50) score += 3;
1032
+ if (id === 'chain-of-responsibility' && /middleware|next\(\)|handler/i.test(codeSnippet)) score += 4;
1033
+ }
1034
+
1035
+ if (score > 0) {
1036
+ results.push({ id, ...pat, score });
1037
+ }
1038
+ }
1039
+
1040
+ results.sort((a, b) => b.score - a.score);
1041
+
1042
+ if (results.length > 0) {
1043
+ const top = results[0];
1044
+ return {
1045
+ matched: 'search',
1046
+ query,
1047
+ pattern: top.name,
1048
+ category: top.category,
1049
+ intent: top.intent,
1050
+ applicability: top.applicability,
1051
+ smellsAddressed: top.smellsAddressed.map(s => SMELLS_CATALOG[s]?.name || s),
1052
+ otherMatches: results.slice(1, 4).map(r => ({ name: r.name, category: r.category })),
1053
+ url: top.url,
1054
+ };
1055
+ }
1056
+
1057
+ return {
1058
+ matched: 'general',
1059
+ query,
1060
+ recommendation: 'Refactoring.Guru Design Patterns: Creational (Factory, Builder, Singleton), Structural (Adapter, Facade, Decorator, Proxy), Behavioral (Strategy, Observer, State, Command).',
1061
+ url: `${GURU_URL}/design-patterns/catalog`,
1062
+ };
1063
+ }
1064
+
1065
+ /**
1066
+ * Format Smell Report as human-readable text.
1067
+ */
1068
+ export function formatSmellReport(report) {
1069
+ if (!report.smellsFound) {
1070
+ return `✔ ${report.filename}: No high-severity code smells detected (clean code).`;
1071
+ }
1072
+ const lines = [`[hydra smell] ${report.filename} (${report.totalLines} lines, ${report.smellsFound} smells)`];
1073
+ for (const f of report.findings) {
1074
+ const loc = f.endLine ? `L${f.line}-L${f.endLine}` : `L${f.line}`;
1075
+ lines.push(` • [${loc}] ${f.name} (${f.family}): ${f.metric}`);
1076
+ lines.push(` ↳ ${f.recipe}`);
1077
+ }
1078
+ return lines.join('\n');
1079
+ }
1080
+
1081
+ /**
1082
+ * Format Recipe as human-readable text.
1083
+ */
1084
+ export function formatRecipe(rec) {
1085
+ const lines = [
1086
+ `[hydra refactor] Recipe for ${rec.symbol} (${rec.totalLines} lines, lines ${rec.startLine}-${rec.endLine})`,
1087
+ ];
1088
+ if (rec.smellsDetected.length) {
1089
+ lines.push(` Detected Smells: ${rec.smellsDetected.join(', ')}`);
1090
+ }
1091
+ for (const s of rec.steps) {
1092
+ lines.push(`\n Step ${s.step}: ${s.technique}`);
1093
+ lines.push(` Goal: ${s.goal}`);
1094
+ for (const inst of s.instructions) {
1095
+ lines.push(` - ${inst}`);
1096
+ }
1097
+ lines.push(` Ref: ${s.url}`);
1098
+ }
1099
+ return lines.join('\n');
1100
+ }
1101
+
1102
+ /**
1103
+ * Format Design Pattern Advisor output as human-readable text.
1104
+ */
1105
+ export function formatPattern(adv) {
1106
+ if (adv.matched === 'general') {
1107
+ return `[hydra design] ${adv.recommendation}\nRef: ${adv.url}`;
1108
+ }
1109
+ const lines = [
1110
+ `[hydra design] ${adv.pattern} Pattern (${adv.category})`,
1111
+ ` Intent: ${adv.intent}`,
1112
+ ` When to use: ${adv.applicability}`,
1113
+ ];
1114
+ if (adv.smellsAddressed?.length) {
1115
+ lines.push(` Addresses Smells: ${adv.smellsAddressed.join(', ')}`);
1116
+ }
1117
+ if (adv.otherMatches?.length) {
1118
+ lines.push(` Other possibilities: ${adv.otherMatches.map(m => `${m.name} (${m.category})`).join(', ')}`);
1119
+ }
1120
+ lines.push(` Guide: ${adv.url}`);
1121
+ return lines.join('\n');
1122
+ }