@thehoneyjar/sigil-diagnostics 0.1.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.
- package/LICENSE.md +660 -0
- package/README.md +128 -0
- package/dist/index.d.ts +312 -0
- package/dist/index.js +931 -0
- package/package.json +59 -0
- package/src/compliance.ts +250 -0
- package/src/detection.ts +373 -0
- package/src/index.ts +48 -0
- package/src/patterns.ts +327 -0
- package/src/service.ts +330 -0
- package/src/types.ts +243 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigil/diagnostics
|
|
3
|
+
*
|
|
4
|
+
* Physics compliance checking and issue detection for Sigil.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
EffectType,
|
|
10
|
+
Severity,
|
|
11
|
+
PatternCategory,
|
|
12
|
+
DiagnosticIssue,
|
|
13
|
+
BehavioralCompliance,
|
|
14
|
+
AnimationCompliance,
|
|
15
|
+
MaterialCompliance,
|
|
16
|
+
ComplianceResult,
|
|
17
|
+
DiagnosticResult,
|
|
18
|
+
PatternCause,
|
|
19
|
+
DiagnosticPattern,
|
|
20
|
+
PatternMatchResult,
|
|
21
|
+
DiagnosticsConfig,
|
|
22
|
+
DiagnosticsService,
|
|
23
|
+
} from './types'
|
|
24
|
+
|
|
25
|
+
export { DiagnosticsError, DiagnosticsErrorCodes } from './types'
|
|
26
|
+
|
|
27
|
+
// Detection
|
|
28
|
+
export { detectEffect, getExpectedPhysics, keywords } from './detection'
|
|
29
|
+
|
|
30
|
+
// Compliance
|
|
31
|
+
export {
|
|
32
|
+
checkBehavioralCompliance,
|
|
33
|
+
checkAnimationCompliance,
|
|
34
|
+
checkMaterialCompliance,
|
|
35
|
+
checkCompliance,
|
|
36
|
+
complianceToIssues,
|
|
37
|
+
isFullyCompliant,
|
|
38
|
+
} from './compliance'
|
|
39
|
+
|
|
40
|
+
// Patterns
|
|
41
|
+
export { PATTERNS, getPatterns, getPatternsByCategory, getPatternById } from './patterns'
|
|
42
|
+
|
|
43
|
+
// Service
|
|
44
|
+
export {
|
|
45
|
+
createDiagnosticsService,
|
|
46
|
+
getDiagnosticsService,
|
|
47
|
+
resetDiagnosticsService,
|
|
48
|
+
} from './service'
|
package/src/patterns.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known Diagnostic Patterns
|
|
3
|
+
*
|
|
4
|
+
* Patterns for detecting common issues in React/Next.js applications.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DiagnosticPattern } from './types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Built-in diagnostic patterns
|
|
11
|
+
*/
|
|
12
|
+
export const PATTERNS: DiagnosticPattern[] = [
|
|
13
|
+
// Hydration Issues
|
|
14
|
+
{
|
|
15
|
+
id: 'hydration-media-query',
|
|
16
|
+
name: 'useMediaQuery Hydration Mismatch',
|
|
17
|
+
category: 'hydration',
|
|
18
|
+
severity: 'error',
|
|
19
|
+
symptoms: [
|
|
20
|
+
'Text content does not match server-rendered HTML',
|
|
21
|
+
'Hydration failed because the initial UI does not match',
|
|
22
|
+
'Component flickers on load',
|
|
23
|
+
'Different content on refresh vs navigation',
|
|
24
|
+
],
|
|
25
|
+
keywords: ['hydration', 'flicker', 'mismatch', 'ssr', 'server'],
|
|
26
|
+
causes: [
|
|
27
|
+
{
|
|
28
|
+
name: 'useMediaQuery SSR mismatch',
|
|
29
|
+
signature: 'useMediaQuery returns false on server, true on client',
|
|
30
|
+
codeSmell: `const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
31
|
+
return isDesktop ? <Dialog /> : <Drawer />;`,
|
|
32
|
+
solution: `// Option 1: Loading state until mounted
|
|
33
|
+
const [mounted, setMounted] = useState(false);
|
|
34
|
+
useEffect(() => setMounted(true), []);
|
|
35
|
+
if (!mounted) return <Skeleton />;
|
|
36
|
+
|
|
37
|
+
// Option 2: CSS-only responsive
|
|
38
|
+
<div className="hidden md:block"><Dialog /></div>
|
|
39
|
+
<div className="md:hidden"><Drawer /></div>`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Date/time in render',
|
|
43
|
+
signature: 'new Date() in render path',
|
|
44
|
+
codeSmell: `return <span>{new Date().toLocaleString()}</span>`,
|
|
45
|
+
solution: `const [time, setTime] = useState<string | null>(null);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setTime(new Date().toLocaleString());
|
|
48
|
+
}, []);
|
|
49
|
+
return <span>{time ?? 'Loading...'}</span>;`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Random values in render',
|
|
53
|
+
signature: 'Math.random() or crypto.randomUUID() in render',
|
|
54
|
+
solution: `// Use useId() for stable IDs
|
|
55
|
+
const id = useId();
|
|
56
|
+
|
|
57
|
+
// Or generate once
|
|
58
|
+
const [randomId] = useState(() => crypto.randomUUID());`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Dialog Issues
|
|
64
|
+
{
|
|
65
|
+
id: 'dialog-instability',
|
|
66
|
+
name: 'Dialog/Modal Instability',
|
|
67
|
+
category: 'dialog',
|
|
68
|
+
severity: 'error',
|
|
69
|
+
symptoms: [
|
|
70
|
+
"Dialog doesn't open reliably",
|
|
71
|
+
'Visual glitch during open/close',
|
|
72
|
+
'Works on desktop, fails on mobile',
|
|
73
|
+
'Absolute positioned elements misaligned',
|
|
74
|
+
'Content jumps or shifts',
|
|
75
|
+
],
|
|
76
|
+
keywords: ['dialog', 'modal', 'drawer', 'glitch', 'popup', 'open', 'close'],
|
|
77
|
+
causes: [
|
|
78
|
+
{
|
|
79
|
+
name: 'ResponsiveDialog hydration',
|
|
80
|
+
signature: 'useMediaQuery controlling Dialog vs Drawer',
|
|
81
|
+
solution: `// Option 1: CSS container queries
|
|
82
|
+
.dialog-content {
|
|
83
|
+
@container (min-width: 768px) {
|
|
84
|
+
/* desktop styles */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Option 2: Consistent loading state
|
|
89
|
+
if (!mounted) return <DialogSkeleton />;`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Absolute positioning context mismatch',
|
|
93
|
+
signature: 'absolute positioning with varying parent chains',
|
|
94
|
+
codeSmell: `<div className="absolute -top-4">Title</div>`,
|
|
95
|
+
solution: `// Ensure explicit positioning context
|
|
96
|
+
<div className="relative">
|
|
97
|
+
<div className="absolute -top-4">Title</div>
|
|
98
|
+
</div>`,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'CSS overflow conflicts',
|
|
102
|
+
signature: 'overflow-auto on parent, overflow-visible on child',
|
|
103
|
+
solution: `// Be explicit at each level
|
|
104
|
+
// Or restructure to avoid conflict
|
|
105
|
+
// Or use style prop for explicit control`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Performance Issues
|
|
111
|
+
{
|
|
112
|
+
id: 'render-performance',
|
|
113
|
+
name: 'Render Performance Issues',
|
|
114
|
+
category: 'performance',
|
|
115
|
+
severity: 'warning',
|
|
116
|
+
symptoms: [
|
|
117
|
+
'Laggy interactions',
|
|
118
|
+
'Delayed response to clicks',
|
|
119
|
+
'Janky animations',
|
|
120
|
+
'UI feels heavy',
|
|
121
|
+
'High INP',
|
|
122
|
+
],
|
|
123
|
+
keywords: ['slow', 'laggy', 'janky', 'performance', 'heavy', 'delay'],
|
|
124
|
+
causes: [
|
|
125
|
+
{
|
|
126
|
+
name: 'Unnecessary re-renders',
|
|
127
|
+
signature: 'Large component tree re-rendering on state change',
|
|
128
|
+
solution: `// Memoize expensive children
|
|
129
|
+
const MemoizedChild = memo(Child);
|
|
130
|
+
|
|
131
|
+
// Colocate state (move it down)
|
|
132
|
+
// Use useMemo for expensive computations
|
|
133
|
+
const processed = useMemo(() => expensiveWork(data), [data]);`,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'Layout thrashing',
|
|
137
|
+
signature: 'Reading layout, writing, reading again',
|
|
138
|
+
solution: `// Batch reads, then batch writes
|
|
139
|
+
// Use requestAnimationFrame
|
|
140
|
+
// Use CSS transforms instead of top/left`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Layout Shift
|
|
146
|
+
{
|
|
147
|
+
id: 'layout-shift',
|
|
148
|
+
name: 'Cumulative Layout Shift (CLS)',
|
|
149
|
+
category: 'layout',
|
|
150
|
+
severity: 'warning',
|
|
151
|
+
symptoms: [
|
|
152
|
+
'Content jumps after load',
|
|
153
|
+
'Buttons move as clicking',
|
|
154
|
+
'High CLS score',
|
|
155
|
+
'Page is jumpy',
|
|
156
|
+
],
|
|
157
|
+
keywords: ['jump', 'shift', 'cls', 'move', 'jumpy'],
|
|
158
|
+
causes: [
|
|
159
|
+
{
|
|
160
|
+
name: 'Images without dimensions',
|
|
161
|
+
signature: '<img> without width/height',
|
|
162
|
+
solution: `<Image
|
|
163
|
+
src={src}
|
|
164
|
+
width={400}
|
|
165
|
+
height={300}
|
|
166
|
+
alt="..."
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
// Or use aspect-ratio
|
|
170
|
+
<div className="aspect-video">
|
|
171
|
+
<img className="object-cover" />
|
|
172
|
+
</div>`,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'Dynamic content without placeholder',
|
|
176
|
+
signature: 'Content loads and pushes things down',
|
|
177
|
+
solution: `// Reserve space
|
|
178
|
+
<div className="min-h-[200px]">
|
|
179
|
+
{loading ? <Skeleton /> : <Content />}
|
|
180
|
+
</div>`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Server Components
|
|
186
|
+
{
|
|
187
|
+
id: 'server-component-error',
|
|
188
|
+
name: 'Server Component Errors',
|
|
189
|
+
category: 'server-component',
|
|
190
|
+
severity: 'error',
|
|
191
|
+
symptoms: [
|
|
192
|
+
'useState is not a function',
|
|
193
|
+
'useEffect is not a function',
|
|
194
|
+
'Cannot use hooks in Server Component',
|
|
195
|
+
'Event handlers cannot be passed',
|
|
196
|
+
],
|
|
197
|
+
keywords: ['server', 'component', 'hook', 'usestate', 'useeffect', 'client'],
|
|
198
|
+
causes: [
|
|
199
|
+
{
|
|
200
|
+
name: 'Hooks in Server Component',
|
|
201
|
+
signature: 'useState/useEffect without "use client"',
|
|
202
|
+
solution: `// Add at top of file
|
|
203
|
+
'use client';
|
|
204
|
+
|
|
205
|
+
// Or extract to Client Component`,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// React 19
|
|
211
|
+
{
|
|
212
|
+
id: 'react-19-changes',
|
|
213
|
+
name: 'React 19 Breaking Changes',
|
|
214
|
+
category: 'react-19',
|
|
215
|
+
severity: 'warning',
|
|
216
|
+
symptoms: ['forwardRef is deprecated', 'Unexpected behavior after upgrade'],
|
|
217
|
+
keywords: ['react 19', 'forwardref', 'upgrade', 'deprecated'],
|
|
218
|
+
causes: [
|
|
219
|
+
{
|
|
220
|
+
name: 'forwardRef deprecated',
|
|
221
|
+
signature: 'Using forwardRef pattern',
|
|
222
|
+
codeSmell: `const Button = forwardRef((props, ref) => ...);`,
|
|
223
|
+
solution: `// ref is now a regular prop
|
|
224
|
+
function Button({ ref, ...props }) {
|
|
225
|
+
return <button ref={ref} {...props} />;
|
|
226
|
+
}`,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Physics Compliance
|
|
232
|
+
{
|
|
233
|
+
id: 'physics-financial-optimistic',
|
|
234
|
+
name: 'Financial Action Using Optimistic Sync',
|
|
235
|
+
category: 'physics',
|
|
236
|
+
severity: 'error',
|
|
237
|
+
symptoms: [
|
|
238
|
+
'Financial action uses optimistic update',
|
|
239
|
+
'Money operation without confirmation',
|
|
240
|
+
'Transaction rolls back after user sees success',
|
|
241
|
+
],
|
|
242
|
+
keywords: [
|
|
243
|
+
'claim',
|
|
244
|
+
'deposit',
|
|
245
|
+
'withdraw',
|
|
246
|
+
'transfer',
|
|
247
|
+
'swap',
|
|
248
|
+
'optimistic',
|
|
249
|
+
],
|
|
250
|
+
causes: [
|
|
251
|
+
{
|
|
252
|
+
name: 'Optimistic update on financial mutation',
|
|
253
|
+
signature: 'onMutate used for financial operations',
|
|
254
|
+
codeSmell: `useMutation({
|
|
255
|
+
mutationFn: claimRewards,
|
|
256
|
+
onMutate: async () => {
|
|
257
|
+
// Optimistic update - WRONG for financial!
|
|
258
|
+
queryClient.setQueryData(['balance'], newBalance)
|
|
259
|
+
}
|
|
260
|
+
})`,
|
|
261
|
+
solution: `// Use pessimistic sync for financial operations
|
|
262
|
+
useMutation({
|
|
263
|
+
mutationFn: claimRewards,
|
|
264
|
+
// NO onMutate - wait for server confirmation
|
|
265
|
+
onSuccess: () => {
|
|
266
|
+
queryClient.invalidateQueries(['balance'])
|
|
267
|
+
}
|
|
268
|
+
})`,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
id: 'physics-destructive-no-confirm',
|
|
275
|
+
name: 'Destructive Action Without Confirmation',
|
|
276
|
+
category: 'physics',
|
|
277
|
+
severity: 'error',
|
|
278
|
+
symptoms: [
|
|
279
|
+
'Delete button with no confirmation',
|
|
280
|
+
'Permanent action happens immediately',
|
|
281
|
+
'No way to undo destructive operation',
|
|
282
|
+
],
|
|
283
|
+
keywords: ['delete', 'remove', 'destroy', 'revoke', 'terminate'],
|
|
284
|
+
causes: [
|
|
285
|
+
{
|
|
286
|
+
name: 'Missing confirmation for destructive action',
|
|
287
|
+
signature: 'Destructive action without confirmation step',
|
|
288
|
+
codeSmell: `<button onClick={() => deleteItem()}>Delete</button>`,
|
|
289
|
+
solution: `// Add confirmation step
|
|
290
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
291
|
+
|
|
292
|
+
return showConfirm ? (
|
|
293
|
+
<ConfirmDialog
|
|
294
|
+
message="Are you sure you want to delete?"
|
|
295
|
+
onConfirm={() => deleteItem()}
|
|
296
|
+
onCancel={() => setShowConfirm(false)}
|
|
297
|
+
/>
|
|
298
|
+
) : (
|
|
299
|
+
<button onClick={() => setShowConfirm(true)}>Delete</button>
|
|
300
|
+
);`,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get all patterns
|
|
308
|
+
*/
|
|
309
|
+
export function getPatterns(): DiagnosticPattern[] {
|
|
310
|
+
return PATTERNS
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get patterns by category
|
|
315
|
+
*/
|
|
316
|
+
export function getPatternsByCategory(
|
|
317
|
+
category: DiagnosticPattern['category']
|
|
318
|
+
): DiagnosticPattern[] {
|
|
319
|
+
return PATTERNS.filter((p) => p.category === category)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get pattern by ID
|
|
324
|
+
*/
|
|
325
|
+
export function getPatternById(id: string): DiagnosticPattern | undefined {
|
|
326
|
+
return PATTERNS.find((p) => p.id === id)
|
|
327
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics Service
|
|
3
|
+
*
|
|
4
|
+
* Main service for physics compliance checking and issue detection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
DiagnosticsService,
|
|
9
|
+
DiagnosticsConfig,
|
|
10
|
+
DiagnosticResult,
|
|
11
|
+
EffectType,
|
|
12
|
+
ComplianceResult,
|
|
13
|
+
PatternMatchResult,
|
|
14
|
+
DiagnosticsError,
|
|
15
|
+
} from './types'
|
|
16
|
+
import { DiagnosticsErrorCodes } from './types'
|
|
17
|
+
import { detectEffect, getExpectedPhysics } from './detection'
|
|
18
|
+
import {
|
|
19
|
+
checkCompliance,
|
|
20
|
+
complianceToIssues,
|
|
21
|
+
isFullyCompliant,
|
|
22
|
+
} from './compliance'
|
|
23
|
+
import { PATTERNS, getPatternById } from './patterns'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default compliance result for when physics aren't specified
|
|
27
|
+
*/
|
|
28
|
+
function createDefaultCompliance(effect: EffectType): ComplianceResult {
|
|
29
|
+
const behavioral = getExpectedPhysics(effect)
|
|
30
|
+
return {
|
|
31
|
+
behavioral: {
|
|
32
|
+
...behavioral,
|
|
33
|
+
compliant: true,
|
|
34
|
+
},
|
|
35
|
+
animation: {
|
|
36
|
+
easing: 'ease-out',
|
|
37
|
+
duration: behavioral.timing,
|
|
38
|
+
compliant: true,
|
|
39
|
+
},
|
|
40
|
+
material: {
|
|
41
|
+
surface: 'elevated',
|
|
42
|
+
shadow: 'soft',
|
|
43
|
+
compliant: true,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a diagnostics service
|
|
50
|
+
*/
|
|
51
|
+
export function createDiagnosticsService(
|
|
52
|
+
anchorClient?: unknown,
|
|
53
|
+
config: DiagnosticsConfig = {}
|
|
54
|
+
): DiagnosticsService {
|
|
55
|
+
const patterns = [...PATTERNS, ...(config.customPatterns ?? [])]
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
async analyze(component: string, code?: string): Promise<DiagnosticResult> {
|
|
59
|
+
// Extract keywords from component name and code
|
|
60
|
+
const keywords = extractKeywords(component, code)
|
|
61
|
+
|
|
62
|
+
// Detect effect type
|
|
63
|
+
const effect = detectEffect(keywords)
|
|
64
|
+
|
|
65
|
+
// Get expected physics for this effect
|
|
66
|
+
const expectedPhysics = getExpectedPhysics(effect)
|
|
67
|
+
|
|
68
|
+
// Check compliance (if code is provided, we could parse it for actual values)
|
|
69
|
+
// For now, we'll use defaults
|
|
70
|
+
const compliance = createDefaultCompliance(effect)
|
|
71
|
+
|
|
72
|
+
// Generate issues from compliance
|
|
73
|
+
const issues = complianceToIssues(compliance)
|
|
74
|
+
|
|
75
|
+
// Match against known patterns if code is provided
|
|
76
|
+
if (code) {
|
|
77
|
+
const patternIssues = matchCodePatterns(code, patterns)
|
|
78
|
+
issues.push(...patternIssues)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Generate suggestions based on effect type
|
|
82
|
+
const suggestions = generateSuggestions(effect, compliance)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
component,
|
|
86
|
+
effect,
|
|
87
|
+
issues,
|
|
88
|
+
compliance,
|
|
89
|
+
suggestions,
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
checkCompliance(
|
|
94
|
+
effect: EffectType,
|
|
95
|
+
physics: Partial<ComplianceResult>
|
|
96
|
+
): boolean {
|
|
97
|
+
const result = checkCompliance(effect, physics)
|
|
98
|
+
return isFullyCompliant(result)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
detectEffect(keywords: string[], types?: string[]): EffectType {
|
|
102
|
+
return detectEffect(keywords, types)
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
matchPatterns(symptoms: string): PatternMatchResult[] {
|
|
106
|
+
const results: PatternMatchResult[] = []
|
|
107
|
+
const lowerSymptoms = symptoms.toLowerCase()
|
|
108
|
+
|
|
109
|
+
for (const pattern of patterns) {
|
|
110
|
+
// Filter by category if specified
|
|
111
|
+
if (config.categories && !config.categories.includes(pattern.category)) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check keywords
|
|
116
|
+
const keywordMatches = pattern.keywords.filter((k) =>
|
|
117
|
+
lowerSymptoms.includes(k.toLowerCase())
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (keywordMatches.length === 0) continue
|
|
121
|
+
|
|
122
|
+
// Check symptoms
|
|
123
|
+
const symptomMatches = pattern.symptoms.filter(
|
|
124
|
+
(s) =>
|
|
125
|
+
lowerSymptoms.includes(s.toLowerCase()) ||
|
|
126
|
+
s
|
|
127
|
+
.toLowerCase()
|
|
128
|
+
.split(' ')
|
|
129
|
+
.some((word) => lowerSymptoms.includes(word))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Calculate confidence
|
|
133
|
+
const confidence = Math.min(
|
|
134
|
+
0.95,
|
|
135
|
+
keywordMatches.length * 0.2 + symptomMatches.length * 0.3
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if (confidence > 0.3) {
|
|
139
|
+
// Pick most likely cause
|
|
140
|
+
const matchedCause = pattern.causes[0]
|
|
141
|
+
|
|
142
|
+
results.push({
|
|
143
|
+
pattern,
|
|
144
|
+
matchedCause,
|
|
145
|
+
confidence,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Sort by confidence
|
|
151
|
+
return results.sort((a, b) => b.confidence - a.confidence)
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
diagnose(symptom: string): string {
|
|
155
|
+
const results = this.matchPatterns(symptom)
|
|
156
|
+
|
|
157
|
+
if (results.length === 0) {
|
|
158
|
+
return "I couldn't match this to a known pattern. Can you describe what's happening in more detail?"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const top = results[0]
|
|
162
|
+
let response = `**Found: ${top.pattern.name}** (${Math.round(top.confidence * 100)}% confidence)\n\n`
|
|
163
|
+
response += `**Cause:** ${top.matchedCause.name}\n\n`
|
|
164
|
+
|
|
165
|
+
if (top.matchedCause.codeSmell) {
|
|
166
|
+
response += `**Code smell:**\n\`\`\`typescript\n${top.matchedCause.codeSmell}\n\`\`\`\n\n`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
response += `**Solution:**\n\`\`\`typescript\n${top.matchedCause.solution}\n\`\`\``
|
|
170
|
+
|
|
171
|
+
return response.trim()
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract keywords from component name and code
|
|
178
|
+
*/
|
|
179
|
+
function extractKeywords(component: string, code?: string): string[] {
|
|
180
|
+
const keywords: string[] = []
|
|
181
|
+
|
|
182
|
+
// Split component name into words
|
|
183
|
+
const componentWords = component
|
|
184
|
+
.replace(/([A-Z])/g, ' $1')
|
|
185
|
+
.toLowerCase()
|
|
186
|
+
.split(/[\s_-]+/)
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
keywords.push(...componentWords)
|
|
189
|
+
|
|
190
|
+
// Extract keywords from code if provided
|
|
191
|
+
if (code) {
|
|
192
|
+
// Look for common patterns
|
|
193
|
+
const patterns = [
|
|
194
|
+
/onClick\s*=\s*\{.*?(delete|remove|save|submit|claim|withdraw)/gi,
|
|
195
|
+
/mutation[Ff]n:\s*.*?(delete|remove|save|submit|claim|withdraw)/gi,
|
|
196
|
+
/useMutation.*?(delete|remove|save|submit|claim|withdraw)/gi,
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
for (const pattern of patterns) {
|
|
200
|
+
const matches = code.match(pattern)
|
|
201
|
+
if (matches) {
|
|
202
|
+
keywords.push(...matches)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return keywords
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Match code against known patterns
|
|
212
|
+
*/
|
|
213
|
+
function matchCodePatterns(
|
|
214
|
+
code: string,
|
|
215
|
+
patterns: typeof PATTERNS
|
|
216
|
+
): import('./types').DiagnosticIssue[] {
|
|
217
|
+
const issues: import('./types').DiagnosticIssue[] = []
|
|
218
|
+
|
|
219
|
+
// Check for optimistic update on financial operations
|
|
220
|
+
if (
|
|
221
|
+
code.includes('onMutate') &&
|
|
222
|
+
(code.includes('claim') ||
|
|
223
|
+
code.includes('withdraw') ||
|
|
224
|
+
code.includes('transfer'))
|
|
225
|
+
) {
|
|
226
|
+
issues.push({
|
|
227
|
+
severity: 'error',
|
|
228
|
+
code: 'FINANCIAL_OPTIMISTIC',
|
|
229
|
+
message:
|
|
230
|
+
'Detected optimistic update (onMutate) on financial operation. Financial operations should use pessimistic sync.',
|
|
231
|
+
suggestion:
|
|
232
|
+
'Remove onMutate and use onSuccess with query invalidation instead.',
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for delete without confirmation
|
|
237
|
+
if (
|
|
238
|
+
code.includes('delete') &&
|
|
239
|
+
!code.includes('confirm') &&
|
|
240
|
+
!code.includes('showConfirm')
|
|
241
|
+
) {
|
|
242
|
+
const hasDirectDelete =
|
|
243
|
+
/onClick\s*=\s*\{.*?delete/i.test(code) ||
|
|
244
|
+
/<button[^>]*>.*?[Dd]elete.*?<\/button>/i.test(code)
|
|
245
|
+
|
|
246
|
+
if (hasDirectDelete) {
|
|
247
|
+
issues.push({
|
|
248
|
+
severity: 'warning',
|
|
249
|
+
code: 'DESTRUCTIVE_NO_CONFIRM',
|
|
250
|
+
message:
|
|
251
|
+
'Delete operation appears to have no confirmation step. Destructive actions should require confirmation.',
|
|
252
|
+
suggestion:
|
|
253
|
+
'Add a confirmation dialog before executing the delete operation.',
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check for useMediaQuery without mount check
|
|
259
|
+
if (code.includes('useMediaQuery') && !code.includes('mounted')) {
|
|
260
|
+
issues.push({
|
|
261
|
+
severity: 'warning',
|
|
262
|
+
code: 'HYDRATION_MEDIA_QUERY',
|
|
263
|
+
message:
|
|
264
|
+
'useMediaQuery without mount check may cause hydration mismatch.',
|
|
265
|
+
suggestion:
|
|
266
|
+
'Add a mounted state check: const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []);',
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return issues
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate suggestions based on effect type and compliance
|
|
275
|
+
*/
|
|
276
|
+
function generateSuggestions(
|
|
277
|
+
effect: EffectType,
|
|
278
|
+
compliance: ComplianceResult
|
|
279
|
+
): string[] {
|
|
280
|
+
const suggestions: string[] = []
|
|
281
|
+
|
|
282
|
+
if (effect === 'financial') {
|
|
283
|
+
suggestions.push('Use pessimistic sync - no onMutate for financial operations')
|
|
284
|
+
suggestions.push('Show amount and confirmation before executing')
|
|
285
|
+
suggestions.push('Invalidate queries on success to refresh balances')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (effect === 'destructive') {
|
|
289
|
+
suggestions.push('Add two-step confirmation before destructive actions')
|
|
290
|
+
suggestions.push('Use 600ms timing for deliberate feel')
|
|
291
|
+
suggestions.push('Provide clear description of what will be deleted')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (effect === 'soft-delete') {
|
|
295
|
+
suggestions.push('Use toast with undo action instead of confirmation dialog')
|
|
296
|
+
suggestions.push('Optimistic update is safe since operation is reversible')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!compliance.behavioral.compliant) {
|
|
300
|
+
suggestions.push(
|
|
301
|
+
`Consider changing sync to ${compliance.behavioral.sync} for ${effect} operations`
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return suggestions
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Default diagnostics service singleton
|
|
310
|
+
*/
|
|
311
|
+
let defaultDiagnosticsService: DiagnosticsService | null = null
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get the default diagnostics service
|
|
315
|
+
*/
|
|
316
|
+
export function getDiagnosticsService(
|
|
317
|
+
anchorClient?: unknown
|
|
318
|
+
): DiagnosticsService {
|
|
319
|
+
if (!defaultDiagnosticsService) {
|
|
320
|
+
defaultDiagnosticsService = createDiagnosticsService(anchorClient)
|
|
321
|
+
}
|
|
322
|
+
return defaultDiagnosticsService
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Reset the default diagnostics service
|
|
327
|
+
*/
|
|
328
|
+
export function resetDiagnosticsService(): void {
|
|
329
|
+
defaultDiagnosticsService = null
|
|
330
|
+
}
|