@codyswann/lisa 1.55.2 → 1.56.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,557 @@
1
+ # Refactoring Patterns
2
+
3
+ This reference provides complete before/after examples for common complexity refactoring scenarios.
4
+
5
+ ## Pattern 1: Flatten Nested Conditionals
6
+
7
+ ### Problem
8
+
9
+ Nested conditional rendering creates exponential complexity:
10
+
11
+ ```tsx
12
+ // Complexity: 6+ (each && and ternary adds complexity, nesting multiplies)
13
+ const BadView = ({ isLoading, hasError, data, isEmpty }: Props) => (
14
+ <Box>
15
+ {isLoading ? (
16
+ <Spinner />
17
+ ) : hasError ? (
18
+ <ErrorState />
19
+ ) : isEmpty ? (
20
+ <EmptyState />
21
+ ) : (
22
+ <Box>
23
+ {data.sections.map(section => (
24
+ <Box key={section.id}>
25
+ {section.isExpanded ? (
26
+ <ExpandedContent items={section.items} />
27
+ ) : (
28
+ <CollapsedHeader title={section.title} />
29
+ )}
30
+ </Box>
31
+ ))}
32
+ </Box>
33
+ )}
34
+ </Box>
35
+ );
36
+ ```
37
+
38
+ ### Solution: Pre-compute State + Flatten
39
+
40
+ ```tsx
41
+ // Container
42
+ type ViewState = "loading" | "error" | "empty" | "ready";
43
+
44
+ const ContainerComponent = () => {
45
+ const { data, loading, error } = useQuery();
46
+
47
+ const viewState = useMemo((): ViewState => {
48
+ if (loading) return "loading";
49
+ if (error) return "error";
50
+ if (!data?.sections?.length) return "empty";
51
+ return "ready";
52
+ }, [loading, error, data?.sections?.length]);
53
+
54
+ const sections = useMemo(() => data?.sections ?? [], [data?.sections]);
55
+
56
+ return <ViewComponent viewState={viewState} sections={sections} />;
57
+ };
58
+
59
+ // View - Complexity reduced to ~3
60
+ const ViewComponent = ({ viewState, sections }: Props) => (
61
+ <Box>
62
+ {viewState === "loading" && <Spinner />}
63
+ {viewState === "error" && <ErrorState />}
64
+ {viewState === "empty" && <EmptyState />}
65
+ {viewState === "ready" && <SectionList sections={sections} />}
66
+ </Box>
67
+ );
68
+ ```
69
+
70
+ ## Pattern 2: Extract Repeated Map Patterns
71
+
72
+ ### Problem
73
+
74
+ Same rendering pattern repeated with different data:
75
+
76
+ ```tsx
77
+ // Complexity: 20+ (each map, conditional, and ternary adds up)
78
+ const FilterModalView = ({ filters, positions, tags, statuses }: Props) => (
79
+ <Box>
80
+ {/* Positions - Pattern A */}
81
+ {positions.length > 0 && (
82
+ <VStack>
83
+ <Text>Positions</Text>
84
+ <HStack>
85
+ {positions.map(p => (
86
+ <Chip
87
+ key={p}
88
+ label={p}
89
+ selected={filters.positions.includes(p)}
90
+ onPress={() => onToggle("position", p)}
91
+ />
92
+ ))}
93
+ </HStack>
94
+ </VStack>
95
+ )}
96
+
97
+ {/* Tags - Pattern A (repeated) */}
98
+ {tags.length > 0 && (
99
+ <VStack>
100
+ <Text>Tags</Text>
101
+ <HStack>
102
+ {tags.map(t => (
103
+ <Chip
104
+ key={t.id}
105
+ label={t.name}
106
+ selected={filters.tags.includes(t.id)}
107
+ onPress={() => onToggle("tag", t.id)}
108
+ />
109
+ ))}
110
+ </HStack>
111
+ </VStack>
112
+ )}
113
+
114
+ {/* Statuses - Pattern A (repeated again) */}
115
+ {statuses.length > 0 && (
116
+ <VStack>
117
+ <Text>Status</Text>
118
+ <HStack>
119
+ {statuses.map(s => (
120
+ <Chip
121
+ key={s}
122
+ label={s}
123
+ selected={filters.statuses.includes(s)}
124
+ onPress={() => onToggle("status", s)}
125
+ />
126
+ ))}
127
+ </HStack>
128
+ </VStack>
129
+ )}
130
+ </Box>
131
+ );
132
+ ```
133
+
134
+ ### Solution: Extract Reusable Component
135
+
136
+ ```tsx
137
+ // New component: FilterSection/FilterSectionContainer.tsx
138
+ interface FilterSectionProps {
139
+ readonly title: string;
140
+ readonly items: readonly { id: string; label: string }[];
141
+ readonly selectedIds: readonly string[];
142
+ readonly onToggle: (id: string) => void;
143
+ }
144
+
145
+ const FilterSectionContainer = ({
146
+ title,
147
+ items,
148
+ selectedIds,
149
+ onToggle,
150
+ }: FilterSectionProps) => {
151
+ const itemsWithSelection = useMemo(
152
+ () =>
153
+ items.map(item => ({
154
+ ...item,
155
+ isSelected: selectedIds.includes(item.id),
156
+ })),
157
+ [items, selectedIds]
158
+ );
159
+
160
+ const handleToggle = useCallback(
161
+ (id: string) => () => onToggle(id),
162
+ [onToggle]
163
+ );
164
+
165
+ if (items.length === 0) return null;
166
+
167
+ return (
168
+ <FilterSectionView
169
+ title={title}
170
+ items={itemsWithSelection}
171
+ onToggle={handleToggle}
172
+ />
173
+ );
174
+ };
175
+
176
+ // FilterSection/FilterSectionView.tsx
177
+ const FilterSectionView = ({ title, items, onToggle }: ViewProps) => (
178
+ <VStack>
179
+ <Text>{title}</Text>
180
+ <HStack>
181
+ {items.map(({ id, label, isSelected }) => (
182
+ <Chip
183
+ key={id}
184
+ label={label}
185
+ selected={isSelected}
186
+ onPress={onToggle(id)}
187
+ />
188
+ ))}
189
+ </HStack>
190
+ </VStack>
191
+ );
192
+
193
+ FilterSectionView.displayName = "FilterSectionView";
194
+ export default memo(FilterSectionView);
195
+
196
+ // Updated parent - Complexity: ~3
197
+ const FilterModalView = ({ positionItems, tagItems, statusItems }: Props) => (
198
+ <Box>
199
+ <FilterSection
200
+ title="Positions"
201
+ items={positionItems}
202
+ selectedIds={filters.positions}
203
+ onToggle={onPositionToggle}
204
+ />
205
+ <FilterSection
206
+ title="Tags"
207
+ items={tagItems}
208
+ selectedIds={filters.tags}
209
+ onToggle={onTagToggle}
210
+ />
211
+ <FilterSection
212
+ title="Status"
213
+ items={statusItems}
214
+ selectedIds={filters.statuses}
215
+ onToggle={onStatusToggle}
216
+ />
217
+ </Box>
218
+ );
219
+ ```
220
+
221
+ ## Pattern 3: Simplify Conditional Styles
222
+
223
+ ### Problem
224
+
225
+ Repeated conditional checks for styling:
226
+
227
+ ```tsx
228
+ // Complexity: 12+ (each includes() and ternary)
229
+ const ChipView = ({ item, selectedItems, colors }: Props) => (
230
+ <Pressable
231
+ style={{
232
+ backgroundColor: selectedItems.includes(item.id)
233
+ ? colors.primary
234
+ : colors.cardBackground,
235
+ borderColor: selectedItems.includes(item.id)
236
+ ? colors.primary
237
+ : colors.border,
238
+ borderWidth: 1,
239
+ paddingHorizontal: 10,
240
+ paddingVertical: 5,
241
+ borderRadius: 6,
242
+ }}
243
+ >
244
+ <Text
245
+ style={{
246
+ color: selectedItems.includes(item.id)
247
+ ? "#FFFFFF"
248
+ : colors.textSecondary,
249
+ fontWeight: selectedItems.includes(item.id) ? "600" : "400",
250
+ }}
251
+ >
252
+ {item.label}
253
+ </Text>
254
+ </Pressable>
255
+ );
256
+ ```
257
+
258
+ ### Solution: Pre-compute and Use Style Objects
259
+
260
+ ```tsx
261
+ // Container - compute selection state once
262
+ const ChipListContainer = ({ items, selectedIds }: Props) => {
263
+ const itemsWithState = useMemo(
264
+ () =>
265
+ items.map(item => ({
266
+ ...item,
267
+ isSelected: selectedIds.includes(item.id),
268
+ })),
269
+ [items, selectedIds]
270
+ );
271
+
272
+ return <ChipListView items={itemsWithState} />;
273
+ };
274
+
275
+ // View - use pre-computed isSelected
276
+ const ChipView = ({ item, isSelected, colors }: Props) => (
277
+ <Pressable
278
+ style={isSelected ? styles.selected(colors) : styles.default(colors)}
279
+ >
280
+ <Text style={isSelected ? styles.selectedText : styles.defaultText(colors)}>
281
+ {item.label}
282
+ </Text>
283
+ </Pressable>
284
+ );
285
+
286
+ // Style helpers (defined outside component)
287
+ const styles = {
288
+ selected: (colors: Colors) => ({
289
+ backgroundColor: colors.primary,
290
+ borderColor: colors.primary,
291
+ borderWidth: 1,
292
+ paddingHorizontal: 10,
293
+ paddingVertical: 5,
294
+ borderRadius: 6,
295
+ }),
296
+ default: (colors: Colors) => ({
297
+ backgroundColor: colors.cardBackground,
298
+ borderColor: colors.border,
299
+ borderWidth: 1,
300
+ paddingHorizontal: 10,
301
+ paddingVertical: 5,
302
+ borderRadius: 6,
303
+ }),
304
+ selectedText: {
305
+ color: "#FFFFFF",
306
+ fontWeight: "600" as const,
307
+ },
308
+ defaultText: (colors: Colors) => ({
309
+ color: colors.textSecondary,
310
+ fontWeight: "400" as const,
311
+ }),
312
+ };
313
+ ```
314
+
315
+ ## Pattern 4: Replace Switch with Object Mapping
316
+
317
+ ### Problem
318
+
319
+ Large switch statements or if-else chains:
320
+
321
+ ```tsx
322
+ // Complexity: 8+ (each case adds complexity)
323
+ const getStatusIcon = (status: string) => {
324
+ switch (status) {
325
+ case "active":
326
+ return <CheckCircle color="green" />;
327
+ case "pending":
328
+ return <Clock color="yellow" />;
329
+ case "inactive":
330
+ return <XCircle color="gray" />;
331
+ case "error":
332
+ return <AlertCircle color="red" />;
333
+ default:
334
+ return <HelpCircle color="gray" />;
335
+ }
336
+ };
337
+
338
+ const StatusView = ({ status }: Props) => <Box>{getStatusIcon(status)}</Box>;
339
+ ```
340
+
341
+ ### Solution: Object Mapping
342
+
343
+ ```tsx
344
+ // Define mapping outside component (no complexity cost)
345
+ const STATUS_ICONS: Record<string, ReactNode> = {
346
+ active: <CheckCircle color="green" />,
347
+ pending: <Clock color="yellow" />,
348
+ inactive: <XCircle color="gray" />,
349
+ error: <AlertCircle color="red" />,
350
+ };
351
+
352
+ const DEFAULT_ICON = <HelpCircle color="gray" />;
353
+
354
+ // View - Complexity: 1
355
+ const StatusView = ({ status }: Props) => (
356
+ <Box>{STATUS_ICONS[status] ?? DEFAULT_ICON}</Box>
357
+ );
358
+ ```
359
+
360
+ ## Pattern 5: Extract Complex Render Sections
361
+
362
+ ### Problem
363
+
364
+ Long View with multiple distinct sections:
365
+
366
+ ```tsx
367
+ // Complexity: 25+ (multiple conditionals, maps, nested structures)
368
+ const DashboardView = ({ user, stats, notifications, activities }: Props) => (
369
+ <Box>
370
+ {/* Header section - 5 lines */}
371
+ <HStack>
372
+ <Avatar source={user.avatar} />
373
+ <VStack>
374
+ <Text>{user.name}</Text>
375
+ <Text>{user.role}</Text>
376
+ </VStack>
377
+ {user.isAdmin && <AdminBadge />}
378
+ </HStack>
379
+
380
+ {/* Stats section - 15 lines */}
381
+ <HStack>
382
+ {stats.map(stat => (
383
+ <Box key={stat.id}>
384
+ <Text>{stat.value}</Text>
385
+ <Text>{stat.label}</Text>
386
+ {stat.trend > 0 ? <TrendUp /> : <TrendDown />}
387
+ </Box>
388
+ ))}
389
+ </HStack>
390
+
391
+ {/* Notifications section - 20 lines */}
392
+ {notifications.length > 0 && (
393
+ <VStack>
394
+ <Text>Notifications ({notifications.length})</Text>
395
+ {notifications.slice(0, 5).map(n => (
396
+ <NotificationItem key={n.id} notification={n} />
397
+ ))}
398
+ {notifications.length > 5 && (
399
+ <Text>+{notifications.length - 5} more</Text>
400
+ )}
401
+ </VStack>
402
+ )}
403
+
404
+ {/* Activities section - 15 lines */}
405
+ <VStack>
406
+ {activities.map(activity => (
407
+ <ActivityRow key={activity.id} activity={activity} />
408
+ ))}
409
+ </VStack>
410
+ </Box>
411
+ );
412
+ ```
413
+
414
+ ### Solution: Extract Helper Functions
415
+
416
+ ```tsx
417
+ /**
418
+ * Renders the user header section.
419
+ * @param props - Section properties
420
+ * @param props.user - User data object
421
+ */
422
+ function renderHeader(props: { readonly user: User }) {
423
+ const { user } = props;
424
+ return (
425
+ <HStack>
426
+ <Avatar source={user.avatar} />
427
+ <VStack>
428
+ <Text>{user.name}</Text>
429
+ <Text>{user.role}</Text>
430
+ </VStack>
431
+ {user.isAdmin && <AdminBadge />}
432
+ </HStack>
433
+ );
434
+ }
435
+
436
+ /**
437
+ * Renders the stats row section.
438
+ * @param props - Section properties
439
+ * @param props.stats - Array of stat objects
440
+ */
441
+ function renderStats(props: { readonly stats: readonly Stat[] }) {
442
+ const { stats } = props;
443
+ return (
444
+ <HStack>
445
+ {stats.map(stat => (
446
+ <Box key={stat.id}>
447
+ <Text>{stat.value}</Text>
448
+ <Text>{stat.label}</Text>
449
+ {stat.trend > 0 ? <TrendUp /> : <TrendDown />}
450
+ </Box>
451
+ ))}
452
+ </HStack>
453
+ );
454
+ }
455
+
456
+ /**
457
+ * Renders the notifications section.
458
+ * @param props - Section properties
459
+ * @param props.notifications - Array of notification objects
460
+ * @param props.maxVisible - Maximum notifications to show before truncating
461
+ */
462
+ function renderNotifications(props: {
463
+ readonly notifications: readonly Notification[];
464
+ readonly maxVisible: number;
465
+ }) {
466
+ const { notifications, maxVisible } = props;
467
+ if (notifications.length === 0) return null;
468
+
469
+ const visible = notifications.slice(0, maxVisible);
470
+ const remaining = notifications.length - maxVisible;
471
+
472
+ return (
473
+ <VStack>
474
+ <Text>Notifications ({notifications.length})</Text>
475
+ {visible.map(n => (
476
+ <NotificationItem key={n.id} notification={n} />
477
+ ))}
478
+ {remaining > 0 && <Text>+{remaining} more</Text>}
479
+ </VStack>
480
+ );
481
+ }
482
+
483
+ // Clean View - Complexity: ~5
484
+ const DashboardView = ({ user, stats, notifications, activities }: Props) => (
485
+ <Box>
486
+ {renderHeader({ user })}
487
+ {renderStats({ stats })}
488
+ {renderNotifications({ notifications, maxVisible: 5 })}
489
+ <ActivityList activities={activities} />
490
+ </Box>
491
+ );
492
+ ```
493
+
494
+ ## Complexity Calculation Reference
495
+
496
+ Understanding how SonarJS calculates cognitive complexity:
497
+
498
+ | Construct | Base Cost | Nesting Penalty |
499
+ | ---------------------------- | --------- | --------------- |
500
+ | `if` / `else if` / `else` | +1 | +1 per level |
501
+ | `? :` (ternary) | +1 | +1 per level |
502
+ | `&&` / `\|\|` (logical) | +1 | +1 per level |
503
+ | `for` / `while` / `do-while` | +1 | +1 per level |
504
+ | `.map()` / `.filter()` etc. | +1 | +1 per level |
505
+ | `catch` | +1 | +1 per level |
506
+ | `switch` | +1 total | - |
507
+ | `case` (in switch) | +1 each | - |
508
+ | `break` / `continue` | +1 | - |
509
+ | Nested function | +1 | +1 per level |
510
+
511
+ ### Example Calculation
512
+
513
+ ```tsx
514
+ const Example = (
515
+ { items, filter }: Props // Base: 0
516
+ ) => (
517
+ <Box>
518
+ {items.length > 0 && ( // +1 (&&)
519
+ <VStack>
520
+ {items // +1 (map, nested in &&)
521
+ .filter(i => i.active) // +1 (filter, nested)
522
+ .map(item => (
523
+ <Box key={item.id}>
524
+ {item.isSpecial ? ( // +1 (ternary, deeply nested)
525
+ <SpecialItem item={item} />
526
+ ) : (
527
+ <RegularItem item={item} />
528
+ )}
529
+ </Box>
530
+ ))}
531
+ </VStack>
532
+ )}
533
+ </Box>
534
+ );
535
+ // Total: 4 + nesting penalties ≈ 8-10
536
+ ```
537
+
538
+ ## Testing After Refactoring
539
+
540
+ Always verify the refactoring didn't break functionality:
541
+
542
+ ```bash
543
+ # 1. Verify lint passes
544
+ bun run lint 2>&1 | grep "cognitive-complexity"
545
+
546
+ # 2. Run unit tests
547
+ bun run test:unit --watch --testPathPattern="ComponentName"
548
+
549
+ # 3. Run full test suite
550
+ bun run test:unit
551
+
552
+ # 4. Manual verification
553
+ bun run start:dev
554
+ # Navigate to the refactored component and test interactions
555
+ ```
556
+
557
+ > **Note:** Replace `bun` with your project's package manager (`npm`, `yarn`, `pnpm`) as needed.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "1.55.2",
3
+ "version": "1.56.0",
4
4
  "description": "Claude Code governance plugin for NestJS/GraphQL projects — includes all universal skills, agents, hooks, and rules from Lisa plus NestJS-specific tooling (GraphQL, TypeORM, security scanning)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "1.55.2",
3
+ "version": "1.56.0",
4
4
  "description": "Claude Code governance plugin for Ruby on Rails projects — includes all universal skills, agents, hooks, and rules from Lisa plus Rails-specific tooling (ActionController, ActionView, ActiveRecord)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "1.55.2",
3
+ "version": "1.56.0",
4
4
  "description": "Claude Code governance plugin for TypeScript projects — includes all universal skills, agents, hooks, and rules from Lisa plus TypeScript-specific tooling",
5
5
  "author": {
6
6
  "name": "Cody Swann"