@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/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
|
+
}
|
package/src/detection.ts
ADDED
|
@@ -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
|
+
}
|