interaqt 0.3.0 → 0.3.1
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/agent/.claude/agents/code-generation-handler.md +2 -0
- package/agent/.claude/agents/computation-generation-handler.md +1 -0
- package/agent/.claude/agents/implement-design-handler.md +4 -13
- package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
- package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
- package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
- package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
- package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
- package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
- package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
- package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
- package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
- package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
- package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
- package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
- package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
- package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
- package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
- package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
- package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
- package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
- package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
- package/agent/agentspace/knowledge/usage/README.md +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# Permission Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Permissions in interaqt are implemented through conditions in interactions. These control who can perform actions and under what circumstances.
|
|
5
|
+
|
|
6
|
+
**Important**: Conditions can return:
|
|
7
|
+
- `true` - Permission granted
|
|
8
|
+
- `false` - Permission denied (generic error)
|
|
9
|
+
- A string - Permission denied with specific error message (recommended)
|
|
10
|
+
|
|
11
|
+
## 🔴 CRITICAL: Common Mistakes
|
|
12
|
+
|
|
13
|
+
### Don't Check Permissions in Computations
|
|
14
|
+
```typescript
|
|
15
|
+
// ❌ WRONG: Permission check in Transform
|
|
16
|
+
Transform.create({
|
|
17
|
+
source: CreateArticle,
|
|
18
|
+
computation: async (event) => {
|
|
19
|
+
if (event.user.role !== 'admin') { // Don't do this!
|
|
20
|
+
throw new Error('Not allowed');
|
|
21
|
+
}
|
|
22
|
+
return { ... };
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// ✅ CORRECT: Use Conditions in Interaction
|
|
27
|
+
const CreateArticle = Interaction.create({
|
|
28
|
+
name: 'CreateArticle',
|
|
29
|
+
conditions: AdminRole // Check here!
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Use MatchExp in Conditions, Not BoolExp
|
|
34
|
+
```typescript
|
|
35
|
+
// ❌ WRONG: Using BoolExp for queries
|
|
36
|
+
const CheckMyPost = Condition.create({
|
|
37
|
+
content: async function(this: Controller, event) {
|
|
38
|
+
// BoolExp is for logic, not queries!
|
|
39
|
+
return BoolExp.atom({ author: event.user })
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// ✅ CORRECT: Use MatchExp for queries
|
|
44
|
+
const CheckMyPost = Condition.create({
|
|
45
|
+
content: async function(this: Controller, event) {
|
|
46
|
+
const post = await this.system.storage.findOne('Post',
|
|
47
|
+
MatchExp.atom({ key: 'author', value: ['=', event.user] })
|
|
48
|
+
)
|
|
49
|
+
return !!post;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Use Conditions.create for BoolExp Combinations
|
|
55
|
+
Remember to wrap BoolExp combinations with Conditions.create:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Conditions } from 'interaqt';
|
|
59
|
+
|
|
60
|
+
conditions: Conditions.create({
|
|
61
|
+
content: BoolExp.atom(isAdmin).and(isActive)
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Basic Role-Based Permissions
|
|
66
|
+
|
|
67
|
+
### 1. Define Role Conditions
|
|
68
|
+
```typescript
|
|
69
|
+
import { Condition, BoolExp, Conditions, Interaction, Action, Payload, PayloadItem, MatchExp } from 'interaqt';
|
|
70
|
+
|
|
71
|
+
// Define role conditions
|
|
72
|
+
export const AdminRole = Condition.create({
|
|
73
|
+
name: 'AdminRole',
|
|
74
|
+
content: async function(this: Controller, event) {
|
|
75
|
+
return event.user?.role === 'admin';
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const OperatorRole = Condition.create({
|
|
80
|
+
name: 'OperatorRole',
|
|
81
|
+
content: async function(this: Controller, event) {
|
|
82
|
+
return event.user?.role === 'operator';
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const ViewerRole = Condition.create({
|
|
87
|
+
name: 'ViewerRole',
|
|
88
|
+
content: async function(this: Controller, event) {
|
|
89
|
+
return event.user?.role === 'viewer';
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Combined permission
|
|
94
|
+
export const OperatorOrAdmin = Condition.create({
|
|
95
|
+
name: 'OperatorOrAdmin',
|
|
96
|
+
content: async function(this: Controller, event) {
|
|
97
|
+
const role = event.user?.role;
|
|
98
|
+
return role === 'admin' || role === 'operator';
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Apply to Interactions
|
|
104
|
+
```typescript
|
|
105
|
+
// Admin-only action
|
|
106
|
+
export const DeleteStyle = Interaction.create({
|
|
107
|
+
name: 'DeleteStyle',
|
|
108
|
+
action: Action.create({ name: 'deleteStyle' }),
|
|
109
|
+
payload: Payload.create({
|
|
110
|
+
items: [
|
|
111
|
+
PayloadItem.create({
|
|
112
|
+
name: 'style',
|
|
113
|
+
base: Style,
|
|
114
|
+
isRef: true
|
|
115
|
+
})
|
|
116
|
+
]
|
|
117
|
+
}),
|
|
118
|
+
conditions: AdminRole // Direct condition
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Multiple roles allowed
|
|
122
|
+
export const UpdateStyle = Interaction.create({
|
|
123
|
+
name: 'UpdateStyle',
|
|
124
|
+
action: Action.create({ name: 'updateStyle' }),
|
|
125
|
+
conditions: OperatorOrAdmin // Combined condition
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Advanced Permission Patterns
|
|
130
|
+
|
|
131
|
+
### 1. Resource Ownership
|
|
132
|
+
```typescript
|
|
133
|
+
// Check if user owns the resource
|
|
134
|
+
export const OwnerOnly = Condition.create({
|
|
135
|
+
name: 'OwnerOnly',
|
|
136
|
+
content: async function(this: Controller, event) {
|
|
137
|
+
const styleId = event.payload?.style?.id;
|
|
138
|
+
if (!styleId) return 'Style ID is required';
|
|
139
|
+
|
|
140
|
+
const style = await this.system.storage.findOne('Style',
|
|
141
|
+
MatchExp.atom({ key: 'id', value: ['=', styleId] }),
|
|
142
|
+
undefined,
|
|
143
|
+
['creator'] // Must include creator field
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (style?.creator?.id !== event.user?.id) {
|
|
147
|
+
return 'You can only modify your own styles';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Combine with role check
|
|
155
|
+
import { BoolExp, Conditions } from 'interaqt';
|
|
156
|
+
|
|
157
|
+
export const DeleteOwnStyle = Interaction.create({
|
|
158
|
+
name: 'DeleteOwnStyle',
|
|
159
|
+
action: Action.create({ name: 'deleteOwnStyle' }),
|
|
160
|
+
payload: Payload.create({
|
|
161
|
+
items: [
|
|
162
|
+
PayloadItem.create({
|
|
163
|
+
name: 'style',
|
|
164
|
+
base: Style,
|
|
165
|
+
isRef: true
|
|
166
|
+
})
|
|
167
|
+
]
|
|
168
|
+
}),
|
|
169
|
+
conditions: Conditions.create({
|
|
170
|
+
content: BoolExp.atom(OwnerOnly).or(BoolExp.atom(AdminRole))
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 2. Context-Based Permissions
|
|
176
|
+
```typescript
|
|
177
|
+
// Check resource state
|
|
178
|
+
export const PublishedOnly = Condition.create({
|
|
179
|
+
name: 'PublishedOnly',
|
|
180
|
+
content: async function(this: Controller, event) {
|
|
181
|
+
const styleId = event.payload?.style?.id;
|
|
182
|
+
if (!styleId) return 'Style ID is required';
|
|
183
|
+
|
|
184
|
+
const style = await this.system.storage.findOne('Style',
|
|
185
|
+
MatchExp.atom({ key: 'id', value: ['=', styleId] }),
|
|
186
|
+
undefined,
|
|
187
|
+
['status'] // Include status field
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (style?.status !== 'published') {
|
|
191
|
+
return 'Style must be published to perform this action';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Apply to interaction
|
|
199
|
+
export const ShareStyle = Interaction.create({
|
|
200
|
+
name: 'ShareStyle',
|
|
201
|
+
action: Action.create({ name: 'shareStyle' }),
|
|
202
|
+
conditions: Conditions.create({
|
|
203
|
+
content: BoolExp.atom(PublishedOnly).and(BoolExp.atom(OperatorOrAdmin))
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 3. Dynamic Permissions
|
|
209
|
+
```typescript
|
|
210
|
+
// Permission based on user's department
|
|
211
|
+
export const SameDepartment = Condition.create({
|
|
212
|
+
name: 'SameDepartment',
|
|
213
|
+
content: async function(this: Controller, event) {
|
|
214
|
+
const targetUserId = event.payload?.targetUser?.id;
|
|
215
|
+
if (!targetUserId) return 'Target user ID is required';
|
|
216
|
+
if (!event.user?.department) return 'Your department information is missing';
|
|
217
|
+
|
|
218
|
+
const targetUser = await this.system.storage.findOne('User',
|
|
219
|
+
MatchExp.atom({ key: 'id', value: ['=', targetUserId] }),
|
|
220
|
+
undefined,
|
|
221
|
+
['department']
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (targetUser?.department !== event.user.department) {
|
|
225
|
+
return 'You can only perform this action on users in your department';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 4. Payload Validation in Conditions
|
|
234
|
+
```typescript
|
|
235
|
+
// Validate payload data within conditions
|
|
236
|
+
export const ValidateStyleType = Condition.create({
|
|
237
|
+
name: 'ValidateStyleType',
|
|
238
|
+
content: async function(this: Controller, event) {
|
|
239
|
+
const styleData = event.payload?.styleData;
|
|
240
|
+
if (!styleData) return 'Style data is required';
|
|
241
|
+
|
|
242
|
+
const validTypes = ['theme', 'component', 'template'];
|
|
243
|
+
if (!validTypes.includes(styleData.type)) {
|
|
244
|
+
return `Invalid style type '${styleData.type}'. Must be one of: ${validTypes.join(', ')}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Combine with user permission check
|
|
252
|
+
export const CreateStyle = Interaction.create({
|
|
253
|
+
name: 'CreateStyle',
|
|
254
|
+
action: Action.create({ name: 'createStyle' }),
|
|
255
|
+
payload: Payload.create({
|
|
256
|
+
items: [
|
|
257
|
+
PayloadItem.create({
|
|
258
|
+
name: 'styleData',
|
|
259
|
+
base: Style
|
|
260
|
+
})
|
|
261
|
+
]
|
|
262
|
+
}),
|
|
263
|
+
conditions: Conditions.create({
|
|
264
|
+
content: BoolExp.atom(OperatorOrAdminRole).and(BoolExp.atom(ValidateStyleType))
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Real-World Example
|
|
270
|
+
```typescript
|
|
271
|
+
import { Condition, BoolExp, Conditions, Interaction, Action, Payload, PayloadItem, MatchExp } from 'interaqt';
|
|
272
|
+
|
|
273
|
+
// Basic conditions
|
|
274
|
+
export const AuthenticatedUser = Condition.create({
|
|
275
|
+
name: 'AuthenticatedUser',
|
|
276
|
+
content: async function(this: Controller, event) {
|
|
277
|
+
if (!event.user || !event.user.id) {
|
|
278
|
+
return 'Authentication required';
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export const ActiveUser = Condition.create({
|
|
285
|
+
name: 'ActiveUser',
|
|
286
|
+
content: async function(this: Controller, event) {
|
|
287
|
+
if (event.user?.status !== 'active') {
|
|
288
|
+
return 'Your account is not active';
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export const AdminRole = Condition.create({
|
|
295
|
+
name: 'AdminRole',
|
|
296
|
+
content: async function(this: Controller, event) {
|
|
297
|
+
if (event.user?.role !== 'admin') {
|
|
298
|
+
return 'Admin privileges required';
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Resource conditions
|
|
305
|
+
export const StyleExists = Condition.create({
|
|
306
|
+
name: 'StyleExists',
|
|
307
|
+
content: async function(this: Controller, event) {
|
|
308
|
+
const styleId = event.payload?.style?.id;
|
|
309
|
+
if (!styleId) return 'Style ID is required';
|
|
310
|
+
|
|
311
|
+
const style = await this.system.storage.findOne('Style',
|
|
312
|
+
MatchExp.atom({ key: 'id', value: ['=', styleId] }),
|
|
313
|
+
undefined,
|
|
314
|
+
['id']
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
if (!style) {
|
|
318
|
+
return 'Style not found';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
export const StyleNotOffline = Condition.create({
|
|
326
|
+
name: 'StyleNotOffline',
|
|
327
|
+
content: async function(this: Controller, event) {
|
|
328
|
+
const styleId = event.payload?.style?.id;
|
|
329
|
+
if (!styleId) return 'Style ID is required';
|
|
330
|
+
|
|
331
|
+
const style = await this.system.storage.findOne('Style',
|
|
332
|
+
MatchExp.atom({ key: 'id', value: ['=', styleId] }),
|
|
333
|
+
undefined,
|
|
334
|
+
['status']
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (style?.status === 'offline') {
|
|
338
|
+
return 'Cannot perform this action on offline styles';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Complex interaction with multiple conditions
|
|
346
|
+
export const PublishStyle = Interaction.create({
|
|
347
|
+
name: 'PublishStyle',
|
|
348
|
+
action: Action.create({ name: 'publish' }),
|
|
349
|
+
payload: Payload.create({
|
|
350
|
+
items: [
|
|
351
|
+
PayloadItem.create({
|
|
352
|
+
name: 'style',
|
|
353
|
+
base: Style,
|
|
354
|
+
isRef: true
|
|
355
|
+
})
|
|
356
|
+
]
|
|
357
|
+
}),
|
|
358
|
+
conditions: Conditions.create({
|
|
359
|
+
content: BoolExp.atom(AuthenticatedUser)
|
|
360
|
+
.and(BoolExp.atom(ActiveUser))
|
|
361
|
+
.and(BoolExp.atom(AdminRole))
|
|
362
|
+
.and(BoolExp.atom(StyleExists))
|
|
363
|
+
.and(BoolExp.atom(StyleNotOffline))
|
|
364
|
+
})
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Alternative: Define combined condition
|
|
368
|
+
export const CanPublishStyle = Conditions.create({
|
|
369
|
+
content: BoolExp.atom(AuthenticatedUser)
|
|
370
|
+
.and(BoolExp.atom(ActiveUser))
|
|
371
|
+
.and(BoolExp.atom(AdminRole))
|
|
372
|
+
.and(BoolExp.atom(StyleExists))
|
|
373
|
+
.and(BoolExp.atom(StyleNotOffline))
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
export const PublishStyleAlt = Interaction.create({
|
|
377
|
+
name: 'PublishStyle',
|
|
378
|
+
action: Action.create({ name: 'publish' }),
|
|
379
|
+
conditions: CanPublishStyle // Reusable condition
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Best Practices
|
|
384
|
+
|
|
385
|
+
### 1. Condition Efficiency
|
|
386
|
+
```typescript
|
|
387
|
+
// ✅ Check simple conditions first
|
|
388
|
+
const EfficientCheck = Condition.create({
|
|
389
|
+
name: 'EfficientCheck',
|
|
390
|
+
content: async function(this: Controller, event) {
|
|
391
|
+
// Simple check first
|
|
392
|
+
if (event.user.role === 'admin') {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Then database query
|
|
397
|
+
const result = await this.system.storage.findOne(...);
|
|
398
|
+
return !!result;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 2. Clear Error Context
|
|
404
|
+
```typescript
|
|
405
|
+
// ✅ Provide meaningful error context
|
|
406
|
+
const WithContext = Condition.create({
|
|
407
|
+
name: 'WithContext',
|
|
408
|
+
content: async function(this: Controller, event) {
|
|
409
|
+
if (!event.user) {
|
|
410
|
+
event.error = 'User not authenticated';
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (event.user.role !== 'admin') {
|
|
415
|
+
event.error = 'Admin role required';
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 3. Reusable Conditions
|
|
425
|
+
```typescript
|
|
426
|
+
// ✅ Create generic, reusable conditions
|
|
427
|
+
export const RequireRole = (role: string) => Condition.create({
|
|
428
|
+
name: `Require${role}Role`,
|
|
429
|
+
content: async function(this: Controller, event) {
|
|
430
|
+
return event.user?.role === role;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Use in multiple places
|
|
435
|
+
const AdminOnly = RequireRole('admin');
|
|
436
|
+
const OperatorOnly = RequireRole('operator');
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### When to Use Conditions.create
|
|
440
|
+
```typescript
|
|
441
|
+
// Use when combining multiple conditions with BoolExp
|
|
442
|
+
conditions: Conditions.create({
|
|
443
|
+
content: BoolExp.atom(condition1)
|
|
444
|
+
.and(BoolExp.atom(condition2))
|
|
445
|
+
.or(BoolExp.atom(condition3))
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// Single condition can be used directly
|
|
449
|
+
conditions: AdminRole
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Security Checklist
|
|
453
|
+
- [ ] Authentication check (user exists)
|
|
454
|
+
- [ ] Authorization check (user has permission)
|
|
455
|
+
- [ ] Resource existence validation
|
|
456
|
+
- [ ] Resource state validation
|
|
457
|
+
- [ ] Payload data validation
|
|
458
|
+
- [ ] BoolExp combinations wrapped with Conditions.create
|
|
459
|
+
- [ ] Efficient condition ordering (simple checks first)
|
|
460
|
+
- [ ] Clear error messages for debugging
|