@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/dist/index.js
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DiagnosticsErrorCodes = {
|
|
3
|
+
ANALYSIS_FAILED: "ANALYSIS_FAILED",
|
|
4
|
+
PATTERN_NOT_FOUND: "PATTERN_NOT_FOUND",
|
|
5
|
+
INVALID_EFFECT: "INVALID_EFFECT",
|
|
6
|
+
ANCHOR_NOT_AVAILABLE: "ANCHOR_NOT_AVAILABLE"
|
|
7
|
+
};
|
|
8
|
+
var DiagnosticsError = class extends Error {
|
|
9
|
+
constructor(message, code, recoverable = true) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.recoverable = recoverable;
|
|
13
|
+
this.name = "DiagnosticsError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/detection.ts
|
|
18
|
+
var FINANCIAL_KEYWORDS = [
|
|
19
|
+
"claim",
|
|
20
|
+
"deposit",
|
|
21
|
+
"withdraw",
|
|
22
|
+
"transfer",
|
|
23
|
+
"swap",
|
|
24
|
+
"send",
|
|
25
|
+
"pay",
|
|
26
|
+
"purchase",
|
|
27
|
+
"mint",
|
|
28
|
+
"burn",
|
|
29
|
+
"stake",
|
|
30
|
+
"unstake",
|
|
31
|
+
"bridge",
|
|
32
|
+
"approve",
|
|
33
|
+
"redeem",
|
|
34
|
+
"harvest",
|
|
35
|
+
"collect",
|
|
36
|
+
"vest",
|
|
37
|
+
"unlock",
|
|
38
|
+
"liquidate",
|
|
39
|
+
"borrow",
|
|
40
|
+
"lend",
|
|
41
|
+
"repay",
|
|
42
|
+
"airdrop",
|
|
43
|
+
"delegate",
|
|
44
|
+
"undelegate",
|
|
45
|
+
"redelegate",
|
|
46
|
+
"bond",
|
|
47
|
+
"unbond",
|
|
48
|
+
"checkout",
|
|
49
|
+
"order",
|
|
50
|
+
"subscribe",
|
|
51
|
+
"upgrade",
|
|
52
|
+
"downgrade",
|
|
53
|
+
"refund"
|
|
54
|
+
];
|
|
55
|
+
var DESTRUCTIVE_KEYWORDS = [
|
|
56
|
+
"delete",
|
|
57
|
+
"remove",
|
|
58
|
+
"destroy",
|
|
59
|
+
"revoke",
|
|
60
|
+
"terminate",
|
|
61
|
+
"purge",
|
|
62
|
+
"erase",
|
|
63
|
+
"wipe",
|
|
64
|
+
"clear",
|
|
65
|
+
"reset",
|
|
66
|
+
"ban",
|
|
67
|
+
"block",
|
|
68
|
+
"suspend",
|
|
69
|
+
"deactivate",
|
|
70
|
+
"cancel",
|
|
71
|
+
"void",
|
|
72
|
+
"invalidate",
|
|
73
|
+
"expire",
|
|
74
|
+
"kill",
|
|
75
|
+
"close account",
|
|
76
|
+
"delete account",
|
|
77
|
+
"remove access",
|
|
78
|
+
"revoke permissions"
|
|
79
|
+
];
|
|
80
|
+
var SOFT_DELETE_KEYWORDS = [
|
|
81
|
+
"archive",
|
|
82
|
+
"hide",
|
|
83
|
+
"trash",
|
|
84
|
+
"dismiss",
|
|
85
|
+
"snooze",
|
|
86
|
+
"mute",
|
|
87
|
+
"silence",
|
|
88
|
+
"ignore",
|
|
89
|
+
"skip",
|
|
90
|
+
"defer",
|
|
91
|
+
"postpone",
|
|
92
|
+
"mark as read",
|
|
93
|
+
"mark as spam",
|
|
94
|
+
"move to folder",
|
|
95
|
+
"soft-delete",
|
|
96
|
+
"temporary hide",
|
|
97
|
+
"pause"
|
|
98
|
+
];
|
|
99
|
+
var STANDARD_KEYWORDS = [
|
|
100
|
+
"save",
|
|
101
|
+
"update",
|
|
102
|
+
"edit",
|
|
103
|
+
"create",
|
|
104
|
+
"add",
|
|
105
|
+
"like",
|
|
106
|
+
"follow",
|
|
107
|
+
"bookmark",
|
|
108
|
+
"favorite",
|
|
109
|
+
"star",
|
|
110
|
+
"pin",
|
|
111
|
+
"tag",
|
|
112
|
+
"label",
|
|
113
|
+
"comment",
|
|
114
|
+
"share",
|
|
115
|
+
"repost",
|
|
116
|
+
"quote",
|
|
117
|
+
"reply",
|
|
118
|
+
"mention",
|
|
119
|
+
"react",
|
|
120
|
+
"submit",
|
|
121
|
+
"post",
|
|
122
|
+
"publish",
|
|
123
|
+
"upload",
|
|
124
|
+
"attach",
|
|
125
|
+
"link",
|
|
126
|
+
"change",
|
|
127
|
+
"modify",
|
|
128
|
+
"set",
|
|
129
|
+
"configure",
|
|
130
|
+
"customize",
|
|
131
|
+
"personalize"
|
|
132
|
+
];
|
|
133
|
+
var LOCAL_KEYWORDS = [
|
|
134
|
+
"toggle",
|
|
135
|
+
"switch",
|
|
136
|
+
"expand",
|
|
137
|
+
"collapse",
|
|
138
|
+
"select",
|
|
139
|
+
"focus",
|
|
140
|
+
"show",
|
|
141
|
+
"hide",
|
|
142
|
+
"open",
|
|
143
|
+
"close",
|
|
144
|
+
"reveal",
|
|
145
|
+
"conceal",
|
|
146
|
+
"check",
|
|
147
|
+
"uncheck",
|
|
148
|
+
"enable",
|
|
149
|
+
"disable",
|
|
150
|
+
"activate",
|
|
151
|
+
"sort",
|
|
152
|
+
"filter",
|
|
153
|
+
"search",
|
|
154
|
+
"zoom",
|
|
155
|
+
"pan",
|
|
156
|
+
"scroll",
|
|
157
|
+
"dark mode",
|
|
158
|
+
"light mode",
|
|
159
|
+
"theme",
|
|
160
|
+
"appearance",
|
|
161
|
+
"display"
|
|
162
|
+
];
|
|
163
|
+
var NAVIGATION_KEYWORDS = [
|
|
164
|
+
"navigate",
|
|
165
|
+
"go",
|
|
166
|
+
"back",
|
|
167
|
+
"forward",
|
|
168
|
+
"link",
|
|
169
|
+
"route",
|
|
170
|
+
"visit",
|
|
171
|
+
"open page",
|
|
172
|
+
"view",
|
|
173
|
+
"browse",
|
|
174
|
+
"explore",
|
|
175
|
+
"next",
|
|
176
|
+
"previous",
|
|
177
|
+
"first",
|
|
178
|
+
"last",
|
|
179
|
+
"jump to",
|
|
180
|
+
"tab",
|
|
181
|
+
"step",
|
|
182
|
+
"page",
|
|
183
|
+
"section",
|
|
184
|
+
"anchor"
|
|
185
|
+
];
|
|
186
|
+
var QUERY_KEYWORDS = [
|
|
187
|
+
"fetch",
|
|
188
|
+
"load",
|
|
189
|
+
"get",
|
|
190
|
+
"list",
|
|
191
|
+
"search",
|
|
192
|
+
"find",
|
|
193
|
+
"query",
|
|
194
|
+
"lookup",
|
|
195
|
+
"retrieve",
|
|
196
|
+
"request",
|
|
197
|
+
"poll",
|
|
198
|
+
"refresh",
|
|
199
|
+
"reload",
|
|
200
|
+
"sync",
|
|
201
|
+
"check status",
|
|
202
|
+
"preview",
|
|
203
|
+
"peek",
|
|
204
|
+
"inspect",
|
|
205
|
+
"examine"
|
|
206
|
+
];
|
|
207
|
+
var FINANCIAL_TYPE_PATTERNS = [
|
|
208
|
+
"Currency",
|
|
209
|
+
"Money",
|
|
210
|
+
"Amount",
|
|
211
|
+
"Wei",
|
|
212
|
+
"BigInt",
|
|
213
|
+
"Token",
|
|
214
|
+
"Balance",
|
|
215
|
+
"Price",
|
|
216
|
+
"Fee"
|
|
217
|
+
];
|
|
218
|
+
var SOFT_DELETE_CONTEXT = ["with undo", "reversible", "recycle bin", "can undo"];
|
|
219
|
+
function matchesKeywords(text, keywords2) {
|
|
220
|
+
const lowerText = text.toLowerCase();
|
|
221
|
+
return keywords2.some((k) => lowerText.includes(k.toLowerCase()));
|
|
222
|
+
}
|
|
223
|
+
function hasFinancialTypes(types) {
|
|
224
|
+
return types.some(
|
|
225
|
+
(t) => FINANCIAL_TYPE_PATTERNS.some(
|
|
226
|
+
(pattern) => t.includes(pattern) || t.toLowerCase().includes(pattern.toLowerCase())
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
function hasSoftDeleteContext(text) {
|
|
231
|
+
const lowerText = text.toLowerCase();
|
|
232
|
+
return SOFT_DELETE_CONTEXT.some((c) => lowerText.includes(c));
|
|
233
|
+
}
|
|
234
|
+
function detectEffect(keywords2, types = []) {
|
|
235
|
+
const combinedText = keywords2.join(" ");
|
|
236
|
+
if (hasFinancialTypes(types)) {
|
|
237
|
+
return "financial";
|
|
238
|
+
}
|
|
239
|
+
if (hasSoftDeleteContext(combinedText)) {
|
|
240
|
+
if (matchesKeywords(combinedText, DESTRUCTIVE_KEYWORDS)) {
|
|
241
|
+
return "soft-delete";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (matchesKeywords(combinedText, FINANCIAL_KEYWORDS)) {
|
|
245
|
+
return "financial";
|
|
246
|
+
}
|
|
247
|
+
if (matchesKeywords(combinedText, DESTRUCTIVE_KEYWORDS)) {
|
|
248
|
+
return "destructive";
|
|
249
|
+
}
|
|
250
|
+
if (matchesKeywords(combinedText, SOFT_DELETE_KEYWORDS)) {
|
|
251
|
+
return "soft-delete";
|
|
252
|
+
}
|
|
253
|
+
if (matchesKeywords(combinedText, LOCAL_KEYWORDS)) {
|
|
254
|
+
return "local";
|
|
255
|
+
}
|
|
256
|
+
if (matchesKeywords(combinedText, NAVIGATION_KEYWORDS)) {
|
|
257
|
+
return "navigation";
|
|
258
|
+
}
|
|
259
|
+
if (matchesKeywords(combinedText, QUERY_KEYWORDS)) {
|
|
260
|
+
return "query";
|
|
261
|
+
}
|
|
262
|
+
if (matchesKeywords(combinedText, STANDARD_KEYWORDS)) {
|
|
263
|
+
return "standard";
|
|
264
|
+
}
|
|
265
|
+
return "standard";
|
|
266
|
+
}
|
|
267
|
+
function getExpectedPhysics(effect) {
|
|
268
|
+
switch (effect) {
|
|
269
|
+
case "financial":
|
|
270
|
+
return { sync: "pessimistic", timing: 800, confirmation: true };
|
|
271
|
+
case "destructive":
|
|
272
|
+
return { sync: "pessimistic", timing: 600, confirmation: true };
|
|
273
|
+
case "soft-delete":
|
|
274
|
+
return { sync: "optimistic", timing: 200, confirmation: false };
|
|
275
|
+
case "standard":
|
|
276
|
+
return { sync: "optimistic", timing: 200, confirmation: false };
|
|
277
|
+
case "navigation":
|
|
278
|
+
return { sync: "immediate", timing: 150, confirmation: false };
|
|
279
|
+
case "query":
|
|
280
|
+
return { sync: "optimistic", timing: 150, confirmation: false };
|
|
281
|
+
case "local":
|
|
282
|
+
return { sync: "immediate", timing: 100, confirmation: false };
|
|
283
|
+
default:
|
|
284
|
+
return { sync: "optimistic", timing: 200, confirmation: false };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
var keywords = {
|
|
288
|
+
financial: FINANCIAL_KEYWORDS,
|
|
289
|
+
destructive: DESTRUCTIVE_KEYWORDS,
|
|
290
|
+
softDelete: SOFT_DELETE_KEYWORDS,
|
|
291
|
+
standard: STANDARD_KEYWORDS,
|
|
292
|
+
local: LOCAL_KEYWORDS,
|
|
293
|
+
navigation: NAVIGATION_KEYWORDS,
|
|
294
|
+
query: QUERY_KEYWORDS
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/compliance.ts
|
|
298
|
+
var EXPECTED_ANIMATION = {
|
|
299
|
+
financial: { easing: "ease-out", duration: 800 },
|
|
300
|
+
destructive: { easing: "ease-out", duration: 600 },
|
|
301
|
+
"soft-delete": { easing: "spring(500)", duration: 200 },
|
|
302
|
+
standard: { easing: "spring(500)", duration: 200 },
|
|
303
|
+
navigation: { easing: "ease", duration: 150 },
|
|
304
|
+
query: { easing: "ease-out", duration: 150 },
|
|
305
|
+
local: { easing: "spring(700)", duration: 100 }
|
|
306
|
+
};
|
|
307
|
+
var EXPECTED_MATERIAL = {
|
|
308
|
+
financial: { surface: "elevated", shadow: "soft" },
|
|
309
|
+
destructive: { surface: "elevated", shadow: "none" },
|
|
310
|
+
"soft-delete": { surface: "flat", shadow: "none" },
|
|
311
|
+
standard: { surface: "elevated", shadow: "soft" },
|
|
312
|
+
navigation: { surface: "flat", shadow: "none" },
|
|
313
|
+
query: { surface: "flat", shadow: "none" },
|
|
314
|
+
local: { surface: "flat", shadow: "none" }
|
|
315
|
+
};
|
|
316
|
+
var TIMING_TOLERANCE = 100;
|
|
317
|
+
function checkBehavioralCompliance(effect, actual) {
|
|
318
|
+
const expected = getExpectedPhysics(effect);
|
|
319
|
+
const syncMatch = actual.sync === expected.sync;
|
|
320
|
+
const timingMatch = actual.timing === void 0 || Math.abs(actual.timing - expected.timing) <= TIMING_TOLERANCE;
|
|
321
|
+
const confirmMatch = actual.confirmation === void 0 || actual.confirmation === expected.confirmation;
|
|
322
|
+
const compliant = syncMatch && timingMatch && confirmMatch;
|
|
323
|
+
let reason;
|
|
324
|
+
if (!compliant) {
|
|
325
|
+
const issues = [];
|
|
326
|
+
if (!syncMatch) {
|
|
327
|
+
issues.push(`sync should be ${expected.sync}, got ${actual.sync}`);
|
|
328
|
+
}
|
|
329
|
+
if (!timingMatch) {
|
|
330
|
+
issues.push(`timing should be ${expected.timing}ms, got ${actual.timing}ms`);
|
|
331
|
+
}
|
|
332
|
+
if (!confirmMatch) {
|
|
333
|
+
issues.push(
|
|
334
|
+
`confirmation should be ${expected.confirmation}, got ${actual.confirmation}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
reason = issues.join("; ");
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
sync: actual.sync ?? expected.sync,
|
|
341
|
+
timing: actual.timing ?? expected.timing,
|
|
342
|
+
confirmation: actual.confirmation ?? expected.confirmation,
|
|
343
|
+
compliant,
|
|
344
|
+
reason
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function checkAnimationCompliance(effect, actual) {
|
|
348
|
+
const expected = EXPECTED_ANIMATION[effect];
|
|
349
|
+
const easingMatch = actual.easing === void 0 || isCompatibleEasing(actual.easing, expected.easing);
|
|
350
|
+
const durationMatch = actual.duration === void 0 || Math.abs(actual.duration - expected.duration) <= TIMING_TOLERANCE;
|
|
351
|
+
const compliant = easingMatch && durationMatch;
|
|
352
|
+
let reason;
|
|
353
|
+
if (!compliant) {
|
|
354
|
+
const issues = [];
|
|
355
|
+
if (!easingMatch) {
|
|
356
|
+
issues.push(`easing should be ${expected.easing}, got ${actual.easing}`);
|
|
357
|
+
}
|
|
358
|
+
if (!durationMatch) {
|
|
359
|
+
issues.push(
|
|
360
|
+
`duration should be ${expected.duration}ms, got ${actual.duration}ms`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
reason = issues.join("; ");
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
easing: actual.easing ?? expected.easing,
|
|
367
|
+
duration: actual.duration ?? expected.duration,
|
|
368
|
+
compliant,
|
|
369
|
+
reason
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function isCompatibleEasing(actual, expected) {
|
|
373
|
+
if (actual === expected)
|
|
374
|
+
return true;
|
|
375
|
+
if (expected.includes("spring") && actual.includes("spring"))
|
|
376
|
+
return true;
|
|
377
|
+
if (expected.includes("ease") && actual.includes("ease"))
|
|
378
|
+
return true;
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
function checkMaterialCompliance(effect, actual) {
|
|
382
|
+
const expected = EXPECTED_MATERIAL[effect];
|
|
383
|
+
const surfaceMatch = actual.surface === void 0 || actual.surface === expected.surface;
|
|
384
|
+
const shadowMatch = actual.shadow === void 0 || actual.shadow === expected.shadow;
|
|
385
|
+
const compliant = surfaceMatch && shadowMatch;
|
|
386
|
+
let reason;
|
|
387
|
+
if (!compliant) {
|
|
388
|
+
const issues = [];
|
|
389
|
+
if (!surfaceMatch) {
|
|
390
|
+
issues.push(`surface should be ${expected.surface}, got ${actual.surface}`);
|
|
391
|
+
}
|
|
392
|
+
if (!shadowMatch) {
|
|
393
|
+
issues.push(`shadow should be ${expected.shadow}, got ${actual.shadow}`);
|
|
394
|
+
}
|
|
395
|
+
reason = issues.join("; ");
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
surface: actual.surface ?? expected.surface,
|
|
399
|
+
shadow: actual.shadow ?? expected.shadow,
|
|
400
|
+
radius: actual.radius,
|
|
401
|
+
compliant,
|
|
402
|
+
reason
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function checkCompliance(effect, physics) {
|
|
406
|
+
return {
|
|
407
|
+
behavioral: checkBehavioralCompliance(effect, physics.behavioral ?? {}),
|
|
408
|
+
animation: checkAnimationCompliance(effect, physics.animation ?? {}),
|
|
409
|
+
material: checkMaterialCompliance(effect, physics.material ?? {})
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function complianceToIssues(compliance) {
|
|
413
|
+
const issues = [];
|
|
414
|
+
if (!compliance.behavioral.compliant && compliance.behavioral.reason) {
|
|
415
|
+
issues.push({
|
|
416
|
+
severity: "error",
|
|
417
|
+
code: "BEHAVIORAL_NONCOMPLIANT",
|
|
418
|
+
message: `Behavioral physics non-compliant: ${compliance.behavioral.reason}`,
|
|
419
|
+
suggestion: "Review sync strategy, timing, and confirmation settings"
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (!compliance.animation.compliant && compliance.animation.reason) {
|
|
423
|
+
issues.push({
|
|
424
|
+
severity: "warning",
|
|
425
|
+
code: "ANIMATION_NONCOMPLIANT",
|
|
426
|
+
message: `Animation physics non-compliant: ${compliance.animation.reason}`,
|
|
427
|
+
suggestion: "Adjust easing and duration to match effect type"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (!compliance.material.compliant && compliance.material.reason) {
|
|
431
|
+
issues.push({
|
|
432
|
+
severity: "info",
|
|
433
|
+
code: "MATERIAL_NONCOMPLIANT",
|
|
434
|
+
message: `Material physics non-compliant: ${compliance.material.reason}`,
|
|
435
|
+
suggestion: "Consider adjusting surface and shadow properties"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return issues;
|
|
439
|
+
}
|
|
440
|
+
function isFullyCompliant(compliance) {
|
|
441
|
+
return compliance.behavioral.compliant && compliance.animation.compliant && compliance.material.compliant;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/patterns.ts
|
|
445
|
+
var PATTERNS = [
|
|
446
|
+
// Hydration Issues
|
|
447
|
+
{
|
|
448
|
+
id: "hydration-media-query",
|
|
449
|
+
name: "useMediaQuery Hydration Mismatch",
|
|
450
|
+
category: "hydration",
|
|
451
|
+
severity: "error",
|
|
452
|
+
symptoms: [
|
|
453
|
+
"Text content does not match server-rendered HTML",
|
|
454
|
+
"Hydration failed because the initial UI does not match",
|
|
455
|
+
"Component flickers on load",
|
|
456
|
+
"Different content on refresh vs navigation"
|
|
457
|
+
],
|
|
458
|
+
keywords: ["hydration", "flicker", "mismatch", "ssr", "server"],
|
|
459
|
+
causes: [
|
|
460
|
+
{
|
|
461
|
+
name: "useMediaQuery SSR mismatch",
|
|
462
|
+
signature: "useMediaQuery returns false on server, true on client",
|
|
463
|
+
codeSmell: `const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
464
|
+
return isDesktop ? <Dialog /> : <Drawer />;`,
|
|
465
|
+
solution: `// Option 1: Loading state until mounted
|
|
466
|
+
const [mounted, setMounted] = useState(false);
|
|
467
|
+
useEffect(() => setMounted(true), []);
|
|
468
|
+
if (!mounted) return <Skeleton />;
|
|
469
|
+
|
|
470
|
+
// Option 2: CSS-only responsive
|
|
471
|
+
<div className="hidden md:block"><Dialog /></div>
|
|
472
|
+
<div className="md:hidden"><Drawer /></div>`
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "Date/time in render",
|
|
476
|
+
signature: "new Date() in render path",
|
|
477
|
+
codeSmell: `return <span>{new Date().toLocaleString()}</span>`,
|
|
478
|
+
solution: `const [time, setTime] = useState<string | null>(null);
|
|
479
|
+
useEffect(() => {
|
|
480
|
+
setTime(new Date().toLocaleString());
|
|
481
|
+
}, []);
|
|
482
|
+
return <span>{time ?? 'Loading...'}</span>;`
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "Random values in render",
|
|
486
|
+
signature: "Math.random() or crypto.randomUUID() in render",
|
|
487
|
+
solution: `// Use useId() for stable IDs
|
|
488
|
+
const id = useId();
|
|
489
|
+
|
|
490
|
+
// Or generate once
|
|
491
|
+
const [randomId] = useState(() => crypto.randomUUID());`
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
// Dialog Issues
|
|
496
|
+
{
|
|
497
|
+
id: "dialog-instability",
|
|
498
|
+
name: "Dialog/Modal Instability",
|
|
499
|
+
category: "dialog",
|
|
500
|
+
severity: "error",
|
|
501
|
+
symptoms: [
|
|
502
|
+
"Dialog doesn't open reliably",
|
|
503
|
+
"Visual glitch during open/close",
|
|
504
|
+
"Works on desktop, fails on mobile",
|
|
505
|
+
"Absolute positioned elements misaligned",
|
|
506
|
+
"Content jumps or shifts"
|
|
507
|
+
],
|
|
508
|
+
keywords: ["dialog", "modal", "drawer", "glitch", "popup", "open", "close"],
|
|
509
|
+
causes: [
|
|
510
|
+
{
|
|
511
|
+
name: "ResponsiveDialog hydration",
|
|
512
|
+
signature: "useMediaQuery controlling Dialog vs Drawer",
|
|
513
|
+
solution: `// Option 1: CSS container queries
|
|
514
|
+
.dialog-content {
|
|
515
|
+
@container (min-width: 768px) {
|
|
516
|
+
/* desktop styles */
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Option 2: Consistent loading state
|
|
521
|
+
if (!mounted) return <DialogSkeleton />;`
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "Absolute positioning context mismatch",
|
|
525
|
+
signature: "absolute positioning with varying parent chains",
|
|
526
|
+
codeSmell: `<div className="absolute -top-4">Title</div>`,
|
|
527
|
+
solution: `// Ensure explicit positioning context
|
|
528
|
+
<div className="relative">
|
|
529
|
+
<div className="absolute -top-4">Title</div>
|
|
530
|
+
</div>`
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "CSS overflow conflicts",
|
|
534
|
+
signature: "overflow-auto on parent, overflow-visible on child",
|
|
535
|
+
solution: `// Be explicit at each level
|
|
536
|
+
// Or restructure to avoid conflict
|
|
537
|
+
// Or use style prop for explicit control`
|
|
538
|
+
}
|
|
539
|
+
]
|
|
540
|
+
},
|
|
541
|
+
// Performance Issues
|
|
542
|
+
{
|
|
543
|
+
id: "render-performance",
|
|
544
|
+
name: "Render Performance Issues",
|
|
545
|
+
category: "performance",
|
|
546
|
+
severity: "warning",
|
|
547
|
+
symptoms: [
|
|
548
|
+
"Laggy interactions",
|
|
549
|
+
"Delayed response to clicks",
|
|
550
|
+
"Janky animations",
|
|
551
|
+
"UI feels heavy",
|
|
552
|
+
"High INP"
|
|
553
|
+
],
|
|
554
|
+
keywords: ["slow", "laggy", "janky", "performance", "heavy", "delay"],
|
|
555
|
+
causes: [
|
|
556
|
+
{
|
|
557
|
+
name: "Unnecessary re-renders",
|
|
558
|
+
signature: "Large component tree re-rendering on state change",
|
|
559
|
+
solution: `// Memoize expensive children
|
|
560
|
+
const MemoizedChild = memo(Child);
|
|
561
|
+
|
|
562
|
+
// Colocate state (move it down)
|
|
563
|
+
// Use useMemo for expensive computations
|
|
564
|
+
const processed = useMemo(() => expensiveWork(data), [data]);`
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "Layout thrashing",
|
|
568
|
+
signature: "Reading layout, writing, reading again",
|
|
569
|
+
solution: `// Batch reads, then batch writes
|
|
570
|
+
// Use requestAnimationFrame
|
|
571
|
+
// Use CSS transforms instead of top/left`
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
},
|
|
575
|
+
// Layout Shift
|
|
576
|
+
{
|
|
577
|
+
id: "layout-shift",
|
|
578
|
+
name: "Cumulative Layout Shift (CLS)",
|
|
579
|
+
category: "layout",
|
|
580
|
+
severity: "warning",
|
|
581
|
+
symptoms: [
|
|
582
|
+
"Content jumps after load",
|
|
583
|
+
"Buttons move as clicking",
|
|
584
|
+
"High CLS score",
|
|
585
|
+
"Page is jumpy"
|
|
586
|
+
],
|
|
587
|
+
keywords: ["jump", "shift", "cls", "move", "jumpy"],
|
|
588
|
+
causes: [
|
|
589
|
+
{
|
|
590
|
+
name: "Images without dimensions",
|
|
591
|
+
signature: "<img> without width/height",
|
|
592
|
+
solution: `<Image
|
|
593
|
+
src={src}
|
|
594
|
+
width={400}
|
|
595
|
+
height={300}
|
|
596
|
+
alt="..."
|
|
597
|
+
/>
|
|
598
|
+
|
|
599
|
+
// Or use aspect-ratio
|
|
600
|
+
<div className="aspect-video">
|
|
601
|
+
<img className="object-cover" />
|
|
602
|
+
</div>`
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: "Dynamic content without placeholder",
|
|
606
|
+
signature: "Content loads and pushes things down",
|
|
607
|
+
solution: `// Reserve space
|
|
608
|
+
<div className="min-h-[200px]">
|
|
609
|
+
{loading ? <Skeleton /> : <Content />}
|
|
610
|
+
</div>`
|
|
611
|
+
}
|
|
612
|
+
]
|
|
613
|
+
},
|
|
614
|
+
// Server Components
|
|
615
|
+
{
|
|
616
|
+
id: "server-component-error",
|
|
617
|
+
name: "Server Component Errors",
|
|
618
|
+
category: "server-component",
|
|
619
|
+
severity: "error",
|
|
620
|
+
symptoms: [
|
|
621
|
+
"useState is not a function",
|
|
622
|
+
"useEffect is not a function",
|
|
623
|
+
"Cannot use hooks in Server Component",
|
|
624
|
+
"Event handlers cannot be passed"
|
|
625
|
+
],
|
|
626
|
+
keywords: ["server", "component", "hook", "usestate", "useeffect", "client"],
|
|
627
|
+
causes: [
|
|
628
|
+
{
|
|
629
|
+
name: "Hooks in Server Component",
|
|
630
|
+
signature: 'useState/useEffect without "use client"',
|
|
631
|
+
solution: `// Add at top of file
|
|
632
|
+
'use client';
|
|
633
|
+
|
|
634
|
+
// Or extract to Client Component`
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
},
|
|
638
|
+
// React 19
|
|
639
|
+
{
|
|
640
|
+
id: "react-19-changes",
|
|
641
|
+
name: "React 19 Breaking Changes",
|
|
642
|
+
category: "react-19",
|
|
643
|
+
severity: "warning",
|
|
644
|
+
symptoms: ["forwardRef is deprecated", "Unexpected behavior after upgrade"],
|
|
645
|
+
keywords: ["react 19", "forwardref", "upgrade", "deprecated"],
|
|
646
|
+
causes: [
|
|
647
|
+
{
|
|
648
|
+
name: "forwardRef deprecated",
|
|
649
|
+
signature: "Using forwardRef pattern",
|
|
650
|
+
codeSmell: `const Button = forwardRef((props, ref) => ...);`,
|
|
651
|
+
solution: `// ref is now a regular prop
|
|
652
|
+
function Button({ ref, ...props }) {
|
|
653
|
+
return <button ref={ref} {...props} />;
|
|
654
|
+
}`
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
},
|
|
658
|
+
// Physics Compliance
|
|
659
|
+
{
|
|
660
|
+
id: "physics-financial-optimistic",
|
|
661
|
+
name: "Financial Action Using Optimistic Sync",
|
|
662
|
+
category: "physics",
|
|
663
|
+
severity: "error",
|
|
664
|
+
symptoms: [
|
|
665
|
+
"Financial action uses optimistic update",
|
|
666
|
+
"Money operation without confirmation",
|
|
667
|
+
"Transaction rolls back after user sees success"
|
|
668
|
+
],
|
|
669
|
+
keywords: [
|
|
670
|
+
"claim",
|
|
671
|
+
"deposit",
|
|
672
|
+
"withdraw",
|
|
673
|
+
"transfer",
|
|
674
|
+
"swap",
|
|
675
|
+
"optimistic"
|
|
676
|
+
],
|
|
677
|
+
causes: [
|
|
678
|
+
{
|
|
679
|
+
name: "Optimistic update on financial mutation",
|
|
680
|
+
signature: "onMutate used for financial operations",
|
|
681
|
+
codeSmell: `useMutation({
|
|
682
|
+
mutationFn: claimRewards,
|
|
683
|
+
onMutate: async () => {
|
|
684
|
+
// Optimistic update - WRONG for financial!
|
|
685
|
+
queryClient.setQueryData(['balance'], newBalance)
|
|
686
|
+
}
|
|
687
|
+
})`,
|
|
688
|
+
solution: `// Use pessimistic sync for financial operations
|
|
689
|
+
useMutation({
|
|
690
|
+
mutationFn: claimRewards,
|
|
691
|
+
// NO onMutate - wait for server confirmation
|
|
692
|
+
onSuccess: () => {
|
|
693
|
+
queryClient.invalidateQueries(['balance'])
|
|
694
|
+
}
|
|
695
|
+
})`
|
|
696
|
+
}
|
|
697
|
+
]
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: "physics-destructive-no-confirm",
|
|
701
|
+
name: "Destructive Action Without Confirmation",
|
|
702
|
+
category: "physics",
|
|
703
|
+
severity: "error",
|
|
704
|
+
symptoms: [
|
|
705
|
+
"Delete button with no confirmation",
|
|
706
|
+
"Permanent action happens immediately",
|
|
707
|
+
"No way to undo destructive operation"
|
|
708
|
+
],
|
|
709
|
+
keywords: ["delete", "remove", "destroy", "revoke", "terminate"],
|
|
710
|
+
causes: [
|
|
711
|
+
{
|
|
712
|
+
name: "Missing confirmation for destructive action",
|
|
713
|
+
signature: "Destructive action without confirmation step",
|
|
714
|
+
codeSmell: `<button onClick={() => deleteItem()}>Delete</button>`,
|
|
715
|
+
solution: `// Add confirmation step
|
|
716
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
717
|
+
|
|
718
|
+
return showConfirm ? (
|
|
719
|
+
<ConfirmDialog
|
|
720
|
+
message="Are you sure you want to delete?"
|
|
721
|
+
onConfirm={() => deleteItem()}
|
|
722
|
+
onCancel={() => setShowConfirm(false)}
|
|
723
|
+
/>
|
|
724
|
+
) : (
|
|
725
|
+
<button onClick={() => setShowConfirm(true)}>Delete</button>
|
|
726
|
+
);`
|
|
727
|
+
}
|
|
728
|
+
]
|
|
729
|
+
}
|
|
730
|
+
];
|
|
731
|
+
function getPatterns() {
|
|
732
|
+
return PATTERNS;
|
|
733
|
+
}
|
|
734
|
+
function getPatternsByCategory(category) {
|
|
735
|
+
return PATTERNS.filter((p) => p.category === category);
|
|
736
|
+
}
|
|
737
|
+
function getPatternById(id) {
|
|
738
|
+
return PATTERNS.find((p) => p.id === id);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/service.ts
|
|
742
|
+
function createDefaultCompliance(effect) {
|
|
743
|
+
const behavioral = getExpectedPhysics(effect);
|
|
744
|
+
return {
|
|
745
|
+
behavioral: {
|
|
746
|
+
...behavioral,
|
|
747
|
+
compliant: true
|
|
748
|
+
},
|
|
749
|
+
animation: {
|
|
750
|
+
easing: "ease-out",
|
|
751
|
+
duration: behavioral.timing,
|
|
752
|
+
compliant: true
|
|
753
|
+
},
|
|
754
|
+
material: {
|
|
755
|
+
surface: "elevated",
|
|
756
|
+
shadow: "soft",
|
|
757
|
+
compliant: true
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function createDiagnosticsService(anchorClient, config = {}) {
|
|
762
|
+
const patterns = [...PATTERNS, ...config.customPatterns ?? []];
|
|
763
|
+
return {
|
|
764
|
+
async analyze(component, code) {
|
|
765
|
+
const keywords2 = extractKeywords(component, code);
|
|
766
|
+
const effect = detectEffect(keywords2);
|
|
767
|
+
const compliance = createDefaultCompliance(effect);
|
|
768
|
+
const issues = complianceToIssues(compliance);
|
|
769
|
+
if (code) {
|
|
770
|
+
const patternIssues = matchCodePatterns(code);
|
|
771
|
+
issues.push(...patternIssues);
|
|
772
|
+
}
|
|
773
|
+
const suggestions = generateSuggestions(effect, compliance);
|
|
774
|
+
return {
|
|
775
|
+
component,
|
|
776
|
+
effect,
|
|
777
|
+
issues,
|
|
778
|
+
compliance,
|
|
779
|
+
suggestions
|
|
780
|
+
};
|
|
781
|
+
},
|
|
782
|
+
checkCompliance(effect, physics) {
|
|
783
|
+
const result = checkCompliance(effect, physics);
|
|
784
|
+
return isFullyCompliant(result);
|
|
785
|
+
},
|
|
786
|
+
detectEffect(keywords2, types) {
|
|
787
|
+
return detectEffect(keywords2, types);
|
|
788
|
+
},
|
|
789
|
+
matchPatterns(symptoms) {
|
|
790
|
+
const results = [];
|
|
791
|
+
const lowerSymptoms = symptoms.toLowerCase();
|
|
792
|
+
for (const pattern of patterns) {
|
|
793
|
+
if (config.categories && !config.categories.includes(pattern.category)) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const keywordMatches = pattern.keywords.filter(
|
|
797
|
+
(k) => lowerSymptoms.includes(k.toLowerCase())
|
|
798
|
+
);
|
|
799
|
+
if (keywordMatches.length === 0)
|
|
800
|
+
continue;
|
|
801
|
+
const symptomMatches = pattern.symptoms.filter(
|
|
802
|
+
(s) => lowerSymptoms.includes(s.toLowerCase()) || s.toLowerCase().split(" ").some((word) => lowerSymptoms.includes(word))
|
|
803
|
+
);
|
|
804
|
+
const confidence = Math.min(
|
|
805
|
+
0.95,
|
|
806
|
+
keywordMatches.length * 0.2 + symptomMatches.length * 0.3
|
|
807
|
+
);
|
|
808
|
+
if (confidence > 0.3) {
|
|
809
|
+
const matchedCause = pattern.causes[0];
|
|
810
|
+
results.push({
|
|
811
|
+
pattern,
|
|
812
|
+
matchedCause,
|
|
813
|
+
confidence
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return results.sort((a, b) => b.confidence - a.confidence);
|
|
818
|
+
},
|
|
819
|
+
diagnose(symptom) {
|
|
820
|
+
const results = this.matchPatterns(symptom);
|
|
821
|
+
if (results.length === 0) {
|
|
822
|
+
return "I couldn't match this to a known pattern. Can you describe what's happening in more detail?";
|
|
823
|
+
}
|
|
824
|
+
const top = results[0];
|
|
825
|
+
let response = `**Found: ${top.pattern.name}** (${Math.round(top.confidence * 100)}% confidence)
|
|
826
|
+
|
|
827
|
+
`;
|
|
828
|
+
response += `**Cause:** ${top.matchedCause.name}
|
|
829
|
+
|
|
830
|
+
`;
|
|
831
|
+
if (top.matchedCause.codeSmell) {
|
|
832
|
+
response += `**Code smell:**
|
|
833
|
+
\`\`\`typescript
|
|
834
|
+
${top.matchedCause.codeSmell}
|
|
835
|
+
\`\`\`
|
|
836
|
+
|
|
837
|
+
`;
|
|
838
|
+
}
|
|
839
|
+
response += `**Solution:**
|
|
840
|
+
\`\`\`typescript
|
|
841
|
+
${top.matchedCause.solution}
|
|
842
|
+
\`\`\``;
|
|
843
|
+
return response.trim();
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function extractKeywords(component, code) {
|
|
848
|
+
const keywords2 = [];
|
|
849
|
+
const componentWords = component.replace(/([A-Z])/g, " $1").toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
850
|
+
keywords2.push(...componentWords);
|
|
851
|
+
if (code) {
|
|
852
|
+
const patterns = [
|
|
853
|
+
/onClick\s*=\s*\{.*?(delete|remove|save|submit|claim|withdraw)/gi,
|
|
854
|
+
/mutation[Ff]n:\s*.*?(delete|remove|save|submit|claim|withdraw)/gi,
|
|
855
|
+
/useMutation.*?(delete|remove|save|submit|claim|withdraw)/gi
|
|
856
|
+
];
|
|
857
|
+
for (const pattern of patterns) {
|
|
858
|
+
const matches = code.match(pattern);
|
|
859
|
+
if (matches) {
|
|
860
|
+
keywords2.push(...matches);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return keywords2;
|
|
865
|
+
}
|
|
866
|
+
function matchCodePatterns(code, patterns) {
|
|
867
|
+
const issues = [];
|
|
868
|
+
if (code.includes("onMutate") && (code.includes("claim") || code.includes("withdraw") || code.includes("transfer"))) {
|
|
869
|
+
issues.push({
|
|
870
|
+
severity: "error",
|
|
871
|
+
code: "FINANCIAL_OPTIMISTIC",
|
|
872
|
+
message: "Detected optimistic update (onMutate) on financial operation. Financial operations should use pessimistic sync.",
|
|
873
|
+
suggestion: "Remove onMutate and use onSuccess with query invalidation instead."
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
if (code.includes("delete") && !code.includes("confirm") && !code.includes("showConfirm")) {
|
|
877
|
+
const hasDirectDelete = /onClick\s*=\s*\{.*?delete/i.test(code) || /<button[^>]*>.*?[Dd]elete.*?<\/button>/i.test(code);
|
|
878
|
+
if (hasDirectDelete) {
|
|
879
|
+
issues.push({
|
|
880
|
+
severity: "warning",
|
|
881
|
+
code: "DESTRUCTIVE_NO_CONFIRM",
|
|
882
|
+
message: "Delete operation appears to have no confirmation step. Destructive actions should require confirmation.",
|
|
883
|
+
suggestion: "Add a confirmation dialog before executing the delete operation."
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (code.includes("useMediaQuery") && !code.includes("mounted")) {
|
|
888
|
+
issues.push({
|
|
889
|
+
severity: "warning",
|
|
890
|
+
code: "HYDRATION_MEDIA_QUERY",
|
|
891
|
+
message: "useMediaQuery without mount check may cause hydration mismatch.",
|
|
892
|
+
suggestion: "Add a mounted state check: const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []);"
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return issues;
|
|
896
|
+
}
|
|
897
|
+
function generateSuggestions(effect, compliance) {
|
|
898
|
+
const suggestions = [];
|
|
899
|
+
if (effect === "financial") {
|
|
900
|
+
suggestions.push("Use pessimistic sync - no onMutate for financial operations");
|
|
901
|
+
suggestions.push("Show amount and confirmation before executing");
|
|
902
|
+
suggestions.push("Invalidate queries on success to refresh balances");
|
|
903
|
+
}
|
|
904
|
+
if (effect === "destructive") {
|
|
905
|
+
suggestions.push("Add two-step confirmation before destructive actions");
|
|
906
|
+
suggestions.push("Use 600ms timing for deliberate feel");
|
|
907
|
+
suggestions.push("Provide clear description of what will be deleted");
|
|
908
|
+
}
|
|
909
|
+
if (effect === "soft-delete") {
|
|
910
|
+
suggestions.push("Use toast with undo action instead of confirmation dialog");
|
|
911
|
+
suggestions.push("Optimistic update is safe since operation is reversible");
|
|
912
|
+
}
|
|
913
|
+
if (!compliance.behavioral.compliant) {
|
|
914
|
+
suggestions.push(
|
|
915
|
+
`Consider changing sync to ${compliance.behavioral.sync} for ${effect} operations`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
return suggestions;
|
|
919
|
+
}
|
|
920
|
+
var defaultDiagnosticsService = null;
|
|
921
|
+
function getDiagnosticsService(anchorClient) {
|
|
922
|
+
if (!defaultDiagnosticsService) {
|
|
923
|
+
defaultDiagnosticsService = createDiagnosticsService();
|
|
924
|
+
}
|
|
925
|
+
return defaultDiagnosticsService;
|
|
926
|
+
}
|
|
927
|
+
function resetDiagnosticsService() {
|
|
928
|
+
defaultDiagnosticsService = null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export { DiagnosticsError, DiagnosticsErrorCodes, PATTERNS, checkAnimationCompliance, checkBehavioralCompliance, checkCompliance, checkMaterialCompliance, complianceToIssues, createDiagnosticsService, detectEffect, getDiagnosticsService, getExpectedPhysics, getPatternById, getPatterns, getPatternsByCategory, isFullyCompliant, keywords, resetDiagnosticsService };
|