@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/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@thehoneyjar/sigil-diagnostics",
3
+ "version": "0.1.0",
4
+ "description": "Physics compliance checking and issue detection for Sigil",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "dependencies": {
20
+ "@thehoneyjar/sigil-anchor": "4.3.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.11.0",
24
+ "tsup": "^8.0.0",
25
+ "typescript": "^5.0.0"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18.0.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "react": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "sideEffects": false,
36
+ "license": "MIT",
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/0xHoneyJar/sigil.git",
44
+ "directory": "packages/diagnostics"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/0xHoneyJar/sigil/issues"
48
+ },
49
+ "homepage": "https://github.com/0xHoneyJar/sigil/tree/main/packages/diagnostics#readme",
50
+ "engines": {
51
+ "node": ">=20.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "typecheck": "tsc --noEmit",
57
+ "clean": "rm -rf dist"
58
+ }
59
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Physics Compliance Checking
3
+ *
4
+ * Verify that component physics match expected values for effect type.
5
+ */
6
+
7
+ import type {
8
+ EffectType,
9
+ ComplianceResult,
10
+ BehavioralCompliance,
11
+ AnimationCompliance,
12
+ MaterialCompliance,
13
+ DiagnosticIssue,
14
+ } from './types'
15
+ import { getExpectedPhysics } from './detection'
16
+
17
+ /**
18
+ * Expected animation values by effect type
19
+ */
20
+ const EXPECTED_ANIMATION: Record<
21
+ EffectType,
22
+ { easing: string; duration: number }
23
+ > = {
24
+ financial: { easing: 'ease-out', duration: 800 },
25
+ destructive: { easing: 'ease-out', duration: 600 },
26
+ 'soft-delete': { easing: 'spring(500)', duration: 200 },
27
+ standard: { easing: 'spring(500)', duration: 200 },
28
+ navigation: { easing: 'ease', duration: 150 },
29
+ query: { easing: 'ease-out', duration: 150 },
30
+ local: { easing: 'spring(700)', duration: 100 },
31
+ }
32
+
33
+ /**
34
+ * Expected material values by effect type
35
+ */
36
+ const EXPECTED_MATERIAL: Record<EffectType, { surface: string; shadow: string }> =
37
+ {
38
+ financial: { surface: 'elevated', shadow: 'soft' },
39
+ destructive: { surface: 'elevated', shadow: 'none' },
40
+ 'soft-delete': { surface: 'flat', shadow: 'none' },
41
+ standard: { surface: 'elevated', shadow: 'soft' },
42
+ navigation: { surface: 'flat', shadow: 'none' },
43
+ query: { surface: 'flat', shadow: 'none' },
44
+ local: { surface: 'flat', shadow: 'none' },
45
+ }
46
+
47
+ /**
48
+ * Timing tolerance for compliance checking (ms)
49
+ */
50
+ const TIMING_TOLERANCE = 100
51
+
52
+ /**
53
+ * Check behavioral physics compliance
54
+ */
55
+ export function checkBehavioralCompliance(
56
+ effect: EffectType,
57
+ actual: Partial<BehavioralCompliance>
58
+ ): BehavioralCompliance {
59
+ const expected = getExpectedPhysics(effect)
60
+
61
+ const syncMatch = actual.sync === expected.sync
62
+ const timingMatch =
63
+ actual.timing === undefined ||
64
+ Math.abs(actual.timing - expected.timing) <= TIMING_TOLERANCE
65
+ const confirmMatch =
66
+ actual.confirmation === undefined ||
67
+ actual.confirmation === expected.confirmation
68
+
69
+ const compliant = syncMatch && timingMatch && confirmMatch
70
+
71
+ let reason: string | undefined
72
+ if (!compliant) {
73
+ const issues: string[] = []
74
+ if (!syncMatch) {
75
+ issues.push(`sync should be ${expected.sync}, got ${actual.sync}`)
76
+ }
77
+ if (!timingMatch) {
78
+ issues.push(`timing should be ${expected.timing}ms, got ${actual.timing}ms`)
79
+ }
80
+ if (!confirmMatch) {
81
+ issues.push(
82
+ `confirmation should be ${expected.confirmation}, got ${actual.confirmation}`
83
+ )
84
+ }
85
+ reason = issues.join('; ')
86
+ }
87
+
88
+ return {
89
+ sync: actual.sync ?? expected.sync,
90
+ timing: actual.timing ?? expected.timing,
91
+ confirmation: actual.confirmation ?? expected.confirmation,
92
+ compliant,
93
+ reason,
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check animation physics compliance
99
+ */
100
+ export function checkAnimationCompliance(
101
+ effect: EffectType,
102
+ actual: Partial<AnimationCompliance>
103
+ ): AnimationCompliance {
104
+ const expected = EXPECTED_ANIMATION[effect]
105
+
106
+ // Easing is more flexible - check if it's in the right family
107
+ const easingMatch =
108
+ actual.easing === undefined || isCompatibleEasing(actual.easing, expected.easing)
109
+
110
+ const durationMatch =
111
+ actual.duration === undefined ||
112
+ Math.abs(actual.duration - expected.duration) <= TIMING_TOLERANCE
113
+
114
+ const compliant = easingMatch && durationMatch
115
+
116
+ let reason: string | undefined
117
+ if (!compliant) {
118
+ const issues: string[] = []
119
+ if (!easingMatch) {
120
+ issues.push(`easing should be ${expected.easing}, got ${actual.easing}`)
121
+ }
122
+ if (!durationMatch) {
123
+ issues.push(
124
+ `duration should be ${expected.duration}ms, got ${actual.duration}ms`
125
+ )
126
+ }
127
+ reason = issues.join('; ')
128
+ }
129
+
130
+ return {
131
+ easing: actual.easing ?? expected.easing,
132
+ duration: actual.duration ?? expected.duration,
133
+ compliant,
134
+ reason,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Check if easing function is compatible with expected
140
+ */
141
+ function isCompatibleEasing(actual: string, expected: string): boolean {
142
+ // Exact match
143
+ if (actual === expected) return true
144
+
145
+ // Spring family
146
+ if (expected.includes('spring') && actual.includes('spring')) return true
147
+
148
+ // Ease family
149
+ if (expected.includes('ease') && actual.includes('ease')) return true
150
+
151
+ return false
152
+ }
153
+
154
+ /**
155
+ * Check material physics compliance
156
+ */
157
+ export function checkMaterialCompliance(
158
+ effect: EffectType,
159
+ actual: Partial<MaterialCompliance>
160
+ ): MaterialCompliance {
161
+ const expected = EXPECTED_MATERIAL[effect]
162
+
163
+ const surfaceMatch = actual.surface === undefined || actual.surface === expected.surface
164
+ const shadowMatch = actual.shadow === undefined || actual.shadow === expected.shadow
165
+
166
+ const compliant = surfaceMatch && shadowMatch
167
+
168
+ let reason: string | undefined
169
+ if (!compliant) {
170
+ const issues: string[] = []
171
+ if (!surfaceMatch) {
172
+ issues.push(`surface should be ${expected.surface}, got ${actual.surface}`)
173
+ }
174
+ if (!shadowMatch) {
175
+ issues.push(`shadow should be ${expected.shadow}, got ${actual.shadow}`)
176
+ }
177
+ reason = issues.join('; ')
178
+ }
179
+
180
+ return {
181
+ surface: actual.surface ?? expected.surface,
182
+ shadow: actual.shadow ?? expected.shadow,
183
+ radius: actual.radius,
184
+ compliant,
185
+ reason,
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Check full compliance across all physics layers
191
+ */
192
+ export function checkCompliance(
193
+ effect: EffectType,
194
+ physics: Partial<ComplianceResult>
195
+ ): ComplianceResult {
196
+ return {
197
+ behavioral: checkBehavioralCompliance(effect, physics.behavioral ?? {}),
198
+ animation: checkAnimationCompliance(effect, physics.animation ?? {}),
199
+ material: checkMaterialCompliance(effect, physics.material ?? {}),
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Generate issues from compliance result
205
+ */
206
+ export function complianceToIssues(
207
+ compliance: ComplianceResult
208
+ ): DiagnosticIssue[] {
209
+ const issues: DiagnosticIssue[] = []
210
+
211
+ if (!compliance.behavioral.compliant && compliance.behavioral.reason) {
212
+ issues.push({
213
+ severity: 'error',
214
+ code: 'BEHAVIORAL_NONCOMPLIANT',
215
+ message: `Behavioral physics non-compliant: ${compliance.behavioral.reason}`,
216
+ suggestion: 'Review sync strategy, timing, and confirmation settings',
217
+ })
218
+ }
219
+
220
+ if (!compliance.animation.compliant && compliance.animation.reason) {
221
+ issues.push({
222
+ severity: 'warning',
223
+ code: 'ANIMATION_NONCOMPLIANT',
224
+ message: `Animation physics non-compliant: ${compliance.animation.reason}`,
225
+ suggestion: 'Adjust easing and duration to match effect type',
226
+ })
227
+ }
228
+
229
+ if (!compliance.material.compliant && compliance.material.reason) {
230
+ issues.push({
231
+ severity: 'info',
232
+ code: 'MATERIAL_NONCOMPLIANT',
233
+ message: `Material physics non-compliant: ${compliance.material.reason}`,
234
+ suggestion: 'Consider adjusting surface and shadow properties',
235
+ })
236
+ }
237
+
238
+ return issues
239
+ }
240
+
241
+ /**
242
+ * Check if compliance result is fully compliant
243
+ */
244
+ export function isFullyCompliant(compliance: ComplianceResult): boolean {
245
+ return (
246
+ compliance.behavioral.compliant &&
247
+ compliance.animation.compliant &&
248
+ compliance.material.compliant
249
+ )
250
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Effect Detection
3
+ *
4
+ * Detect effect type from keywords, types, and context.
5
+ */
6
+
7
+ import type { EffectType } from './types'
8
+
9
+ /**
10
+ * Keywords that indicate financial operations
11
+ */
12
+ const FINANCIAL_KEYWORDS = [
13
+ 'claim',
14
+ 'deposit',
15
+ 'withdraw',
16
+ 'transfer',
17
+ 'swap',
18
+ 'send',
19
+ 'pay',
20
+ 'purchase',
21
+ 'mint',
22
+ 'burn',
23
+ 'stake',
24
+ 'unstake',
25
+ 'bridge',
26
+ 'approve',
27
+ 'redeem',
28
+ 'harvest',
29
+ 'collect',
30
+ 'vest',
31
+ 'unlock',
32
+ 'liquidate',
33
+ 'borrow',
34
+ 'lend',
35
+ 'repay',
36
+ 'airdrop',
37
+ 'delegate',
38
+ 'undelegate',
39
+ 'redelegate',
40
+ 'bond',
41
+ 'unbond',
42
+ 'checkout',
43
+ 'order',
44
+ 'subscribe',
45
+ 'upgrade',
46
+ 'downgrade',
47
+ 'refund',
48
+ ]
49
+
50
+ /**
51
+ * Keywords that indicate destructive operations
52
+ */
53
+ const DESTRUCTIVE_KEYWORDS = [
54
+ 'delete',
55
+ 'remove',
56
+ 'destroy',
57
+ 'revoke',
58
+ 'terminate',
59
+ 'purge',
60
+ 'erase',
61
+ 'wipe',
62
+ 'clear',
63
+ 'reset',
64
+ 'ban',
65
+ 'block',
66
+ 'suspend',
67
+ 'deactivate',
68
+ 'cancel',
69
+ 'void',
70
+ 'invalidate',
71
+ 'expire',
72
+ 'kill',
73
+ 'close account',
74
+ 'delete account',
75
+ 'remove access',
76
+ 'revoke permissions',
77
+ ]
78
+
79
+ /**
80
+ * Keywords that indicate soft delete operations
81
+ */
82
+ const SOFT_DELETE_KEYWORDS = [
83
+ 'archive',
84
+ 'hide',
85
+ 'trash',
86
+ 'dismiss',
87
+ 'snooze',
88
+ 'mute',
89
+ 'silence',
90
+ 'ignore',
91
+ 'skip',
92
+ 'defer',
93
+ 'postpone',
94
+ 'mark as read',
95
+ 'mark as spam',
96
+ 'move to folder',
97
+ 'soft-delete',
98
+ 'temporary hide',
99
+ 'pause',
100
+ ]
101
+
102
+ /**
103
+ * Keywords that indicate standard operations
104
+ */
105
+ const STANDARD_KEYWORDS = [
106
+ 'save',
107
+ 'update',
108
+ 'edit',
109
+ 'create',
110
+ 'add',
111
+ 'like',
112
+ 'follow',
113
+ 'bookmark',
114
+ 'favorite',
115
+ 'star',
116
+ 'pin',
117
+ 'tag',
118
+ 'label',
119
+ 'comment',
120
+ 'share',
121
+ 'repost',
122
+ 'quote',
123
+ 'reply',
124
+ 'mention',
125
+ 'react',
126
+ 'submit',
127
+ 'post',
128
+ 'publish',
129
+ 'upload',
130
+ 'attach',
131
+ 'link',
132
+ 'change',
133
+ 'modify',
134
+ 'set',
135
+ 'configure',
136
+ 'customize',
137
+ 'personalize',
138
+ ]
139
+
140
+ /**
141
+ * Keywords that indicate local state operations
142
+ */
143
+ const LOCAL_KEYWORDS = [
144
+ 'toggle',
145
+ 'switch',
146
+ 'expand',
147
+ 'collapse',
148
+ 'select',
149
+ 'focus',
150
+ 'show',
151
+ 'hide',
152
+ 'open',
153
+ 'close',
154
+ 'reveal',
155
+ 'conceal',
156
+ 'check',
157
+ 'uncheck',
158
+ 'enable',
159
+ 'disable',
160
+ 'activate',
161
+ 'sort',
162
+ 'filter',
163
+ 'search',
164
+ 'zoom',
165
+ 'pan',
166
+ 'scroll',
167
+ 'dark mode',
168
+ 'light mode',
169
+ 'theme',
170
+ 'appearance',
171
+ 'display',
172
+ ]
173
+
174
+ /**
175
+ * Keywords that indicate navigation
176
+ */
177
+ const NAVIGATION_KEYWORDS = [
178
+ 'navigate',
179
+ 'go',
180
+ 'back',
181
+ 'forward',
182
+ 'link',
183
+ 'route',
184
+ 'visit',
185
+ 'open page',
186
+ 'view',
187
+ 'browse',
188
+ 'explore',
189
+ 'next',
190
+ 'previous',
191
+ 'first',
192
+ 'last',
193
+ 'jump to',
194
+ 'tab',
195
+ 'step',
196
+ 'page',
197
+ 'section',
198
+ 'anchor',
199
+ ]
200
+
201
+ /**
202
+ * Keywords that indicate query operations
203
+ */
204
+ const QUERY_KEYWORDS = [
205
+ 'fetch',
206
+ 'load',
207
+ 'get',
208
+ 'list',
209
+ 'search',
210
+ 'find',
211
+ 'query',
212
+ 'lookup',
213
+ 'retrieve',
214
+ 'request',
215
+ 'poll',
216
+ 'refresh',
217
+ 'reload',
218
+ 'sync',
219
+ 'check status',
220
+ 'preview',
221
+ 'peek',
222
+ 'inspect',
223
+ 'examine',
224
+ ]
225
+
226
+ /**
227
+ * Type patterns that override keyword detection
228
+ */
229
+ const FINANCIAL_TYPE_PATTERNS = [
230
+ 'Currency',
231
+ 'Money',
232
+ 'Amount',
233
+ 'Wei',
234
+ 'BigInt',
235
+ 'Token',
236
+ 'Balance',
237
+ 'Price',
238
+ 'Fee',
239
+ ]
240
+
241
+ /**
242
+ * Context phrases that modify detection
243
+ */
244
+ const SOFT_DELETE_CONTEXT = ['with undo', 'reversible', 'recycle bin', 'can undo']
245
+
246
+ /**
247
+ * Check if any keyword matches
248
+ */
249
+ function matchesKeywords(text: string, keywords: string[]): boolean {
250
+ const lowerText = text.toLowerCase()
251
+ return keywords.some((k) => lowerText.includes(k.toLowerCase()))
252
+ }
253
+
254
+ /**
255
+ * Check if types indicate financial operations
256
+ */
257
+ function hasFinancialTypes(types: string[]): boolean {
258
+ return types.some((t) =>
259
+ FINANCIAL_TYPE_PATTERNS.some(
260
+ (pattern) =>
261
+ t.includes(pattern) || t.toLowerCase().includes(pattern.toLowerCase())
262
+ )
263
+ )
264
+ }
265
+
266
+ /**
267
+ * Check for soft delete context
268
+ */
269
+ function hasSoftDeleteContext(text: string): boolean {
270
+ const lowerText = text.toLowerCase()
271
+ return SOFT_DELETE_CONTEXT.some((c) => lowerText.includes(c))
272
+ }
273
+
274
+ /**
275
+ * Detect effect type from keywords and types
276
+ *
277
+ * Priority:
278
+ * 1. Types override keywords (Currency, Wei, etc. → Financial)
279
+ * 2. Context can modify detection (with undo → Soft Delete)
280
+ * 3. Keywords determine base effect
281
+ */
282
+ export function detectEffect(
283
+ keywords: string[],
284
+ types: string[] = []
285
+ ): EffectType {
286
+ const combinedText = keywords.join(' ')
287
+
288
+ // 1. Type overrides - if financial types present, always financial
289
+ if (hasFinancialTypes(types)) {
290
+ return 'financial'
291
+ }
292
+
293
+ // 2. Context check - "with undo" converts destructive to soft-delete
294
+ if (hasSoftDeleteContext(combinedText)) {
295
+ // Only if it was going to be destructive
296
+ if (matchesKeywords(combinedText, DESTRUCTIVE_KEYWORDS)) {
297
+ return 'soft-delete'
298
+ }
299
+ }
300
+
301
+ // 3. Keyword detection in priority order
302
+ if (matchesKeywords(combinedText, FINANCIAL_KEYWORDS)) {
303
+ return 'financial'
304
+ }
305
+
306
+ if (matchesKeywords(combinedText, DESTRUCTIVE_KEYWORDS)) {
307
+ return 'destructive'
308
+ }
309
+
310
+ if (matchesKeywords(combinedText, SOFT_DELETE_KEYWORDS)) {
311
+ return 'soft-delete'
312
+ }
313
+
314
+ if (matchesKeywords(combinedText, LOCAL_KEYWORDS)) {
315
+ return 'local'
316
+ }
317
+
318
+ if (matchesKeywords(combinedText, NAVIGATION_KEYWORDS)) {
319
+ return 'navigation'
320
+ }
321
+
322
+ if (matchesKeywords(combinedText, QUERY_KEYWORDS)) {
323
+ return 'query'
324
+ }
325
+
326
+ if (matchesKeywords(combinedText, STANDARD_KEYWORDS)) {
327
+ return 'standard'
328
+ }
329
+
330
+ // Default to standard
331
+ return 'standard'
332
+ }
333
+
334
+ /**
335
+ * Get expected physics for an effect type
336
+ */
337
+ export function getExpectedPhysics(effect: EffectType): {
338
+ sync: 'optimistic' | 'pessimistic' | 'immediate'
339
+ timing: number
340
+ confirmation: boolean
341
+ } {
342
+ switch (effect) {
343
+ case 'financial':
344
+ return { sync: 'pessimistic', timing: 800, confirmation: true }
345
+ case 'destructive':
346
+ return { sync: 'pessimistic', timing: 600, confirmation: true }
347
+ case 'soft-delete':
348
+ return { sync: 'optimistic', timing: 200, confirmation: false }
349
+ case 'standard':
350
+ return { sync: 'optimistic', timing: 200, confirmation: false }
351
+ case 'navigation':
352
+ return { sync: 'immediate', timing: 150, confirmation: false }
353
+ case 'query':
354
+ return { sync: 'optimistic', timing: 150, confirmation: false }
355
+ case 'local':
356
+ return { sync: 'immediate', timing: 100, confirmation: false }
357
+ default:
358
+ return { sync: 'optimistic', timing: 200, confirmation: false }
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Export keyword lists for external use
364
+ */
365
+ export const keywords = {
366
+ financial: FINANCIAL_KEYWORDS,
367
+ destructive: DESTRUCTIVE_KEYWORDS,
368
+ softDelete: SOFT_DELETE_KEYWORDS,
369
+ standard: STANDARD_KEYWORDS,
370
+ local: LOCAL_KEYWORDS,
371
+ navigation: NAVIGATION_KEYWORDS,
372
+ query: QUERY_KEYWORDS,
373
+ }