@sladg/apex-state 4.0.0 → 4.6.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/README.md +106 -215
- package/dist/index.d.ts +191 -363
- package/dist/index.js +2 -3404
- package/dist/index.js.map +1 -1
- package/dist/preload.d.ts +2 -0
- package/dist/preload.js +3 -0
- package/dist/preload.js.map +1 -0
- package/dist/testing/index.js +2 -2015
- package/dist/testing/index.js.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sladg/apex-state
|
|
2
2
|
|
|
3
|
-
Reactive state management for React built on [Valtio](https://github.com/pmndrs/valtio). Declare what your fields need — validation, conditional UI, sync, listeners — and the store handles the rest.
|
|
3
|
+
Reactive state management for React built on [Valtio](https://github.com/pmndrs/valtio). Declare what your fields need — validation, conditional UI, sync, listeners — and the store handles the rest. Rust/WASM pipeline handles all heavy computation: graph traversal, expression evaluation, listener dispatch.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -59,13 +59,17 @@ const UserForm = () => {
|
|
|
59
59
|
| Feature | Description | Details |
|
|
60
60
|
|---|---|---|
|
|
61
61
|
| **Type-safe paths** | `DeepKey<T>` / `DeepValue<T, P>` — compile-time path safety with configurable depth | |
|
|
62
|
-
| **Concerns** | Validation (Zod), BoolLogic conditions, dynamic text | [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) |
|
|
62
|
+
| **Concerns** | Validation (Zod), BoolLogic conditions, ValueLogic conditional values, dynamic text | [Concerns Guide](docs/guides/CONCERNS_GUIDE.md) |
|
|
63
63
|
| **Side effects** | Sync paths, flip paths, aggregations, listeners | [Side Effects Guide](docs/SIDE_EFFECTS_GUIDE.md) |
|
|
64
|
-
| **WASM mode** | Rust-powered pipeline for bulk operations (
|
|
64
|
+
| **WASM mode** | Rust-powered pipeline for bulk operations (full catalog refresh in 7.4ms) | [Architecture](docs/WASM_ARCHITECTURE.md) |
|
|
65
65
|
| **Composable hooks** | Buffered, throttled, transformed field wrappers | [Store & Hooks](docs/guides/STORE_HOOKS.md) |
|
|
66
66
|
| **Record/wildcard** | `Record<string, V>` with `_()` hash key paths | [Wildcard Guide](docs/WILD_FUNCTION_GUIDE.md) |
|
|
67
67
|
| **Testing mock** | Drop-in `vi.mock` replacement with call tracking | [Testing Mock](docs/TESTING_MOCK.md) |
|
|
68
68
|
|
|
69
|
+
**Side effects** enforce data invariants — when X changes, Y must follow (sync, flip, aggregate, listen). **Concerns** annotate fields with UI meaning — given settled state, is this field valid, disabled, visible? They run at different times and write to different proxies; neither drives the other.
|
|
70
|
+
|
|
71
|
+
Side effects and declarative concerns (`boolLogic`, `valueLogic`, `validationState`) are evaluated **synchronously inside the WASM pipeline** — results are available immediately after the state change. Custom `evaluate()` concerns use valtio's `effect()` and are **not guaranteed synchronous** — valtio may batch or defer their evaluation to the next microtask.
|
|
72
|
+
|
|
69
73
|
## Full Example
|
|
70
74
|
|
|
71
75
|
```tsx
|
|
@@ -125,214 +129,101 @@ const App = () => (
|
|
|
125
129
|
)
|
|
126
130
|
```
|
|
127
131
|
|
|
128
|
-
##
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
const store = createGenericStore<MyState>()
|
|
132
|
-
|
|
133
|
-
// useStore — simple [value, setter] tuple (like useState)
|
|
134
|
-
const NameInput = () => {
|
|
135
|
-
const [name, setName] = store.useStore('user.name')
|
|
136
|
-
return <input value={name} onChange={(e) => setName(e.target.value)} />
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// useFieldStore — object API with concerns merged in
|
|
140
|
-
const EmailInput = () => {
|
|
141
|
-
const { value, setValue, validationState, disabledWhen } =
|
|
142
|
-
store.useFieldStore('user.email')
|
|
143
|
-
return (
|
|
144
|
-
<input
|
|
145
|
-
value={value}
|
|
146
|
-
onChange={(e) => setValue(e.target.value)}
|
|
147
|
-
disabled={disabledWhen}
|
|
148
|
-
className={validationState?.isError ? 'error' : ''}
|
|
149
|
-
/>
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// useJitStore — bulk operations and non-reactive reads
|
|
154
|
-
const ImportButton = () => {
|
|
155
|
-
const { setChanges, getState } = store.useJitStore()
|
|
156
|
-
|
|
157
|
-
const handleImport = (data: Record<string, unknown>) => {
|
|
158
|
-
setChanges([
|
|
159
|
-
['user.name', data.name, {}],
|
|
160
|
-
['user.email', data.email, {}],
|
|
161
|
-
['user.age', data.age, {}],
|
|
162
|
-
])
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return <button onClick={() => handleImport({ name: 'Alice', email: 'a@b.com', age: 30 })}>Import</button>
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## Validation with Zod
|
|
170
|
-
|
|
171
|
-
```tsx
|
|
172
|
-
import { createGenericStore } from '@sladg/apex-state'
|
|
173
|
-
import { z } from 'zod'
|
|
174
|
-
|
|
175
|
-
type ProfileState = {
|
|
176
|
-
user: { name: string; email: string; age: number; bio: string }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const store = createGenericStore<ProfileState>()
|
|
180
|
-
|
|
181
|
-
const ProfileForm = () => {
|
|
182
|
-
// Register validation schemas for multiple fields at once
|
|
183
|
-
store.useConcerns('profile-validation', {
|
|
184
|
-
'user.name': {
|
|
185
|
-
validationState: { schema: z.string().min(2, 'Name too short').max(50) },
|
|
186
|
-
},
|
|
187
|
-
'user.email': {
|
|
188
|
-
validationState: { schema: z.string().email('Enter a valid email') },
|
|
189
|
-
},
|
|
190
|
-
'user.age': {
|
|
191
|
-
validationState: { schema: z.number().min(18, 'Must be 18+').max(120) },
|
|
192
|
-
},
|
|
193
|
-
'user.bio': {
|
|
194
|
-
validationState: { schema: z.string().max(500, 'Bio too long') },
|
|
195
|
-
},
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
const email = store.useFieldStore('user.email')
|
|
199
|
-
const age = store.useFieldStore('user.age')
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
<form>
|
|
203
|
-
<div>
|
|
204
|
-
<input value={email.value} onChange={(e) => email.setValue(e.target.value)} />
|
|
205
|
-
{email.validationState?.isError && (
|
|
206
|
-
<ul>{email.validationState.errors.map((err, i) => <li key={i}>{err.message}</li>)}</ul>
|
|
207
|
-
)}
|
|
208
|
-
</div>
|
|
209
|
-
<div>
|
|
210
|
-
<input
|
|
211
|
-
type="number"
|
|
212
|
-
value={age.value}
|
|
213
|
-
onChange={(e) => age.setValue(Number(e.target.value))}
|
|
214
|
-
/>
|
|
215
|
-
{age.validationState?.isError && <span>{age.validationState.errors[0]?.message}</span>}
|
|
216
|
-
</div>
|
|
217
|
-
</form>
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
```
|
|
132
|
+
## Concerns
|
|
221
133
|
|
|
222
|
-
|
|
134
|
+
Concerns describe *what a field means* for the UI given the current state — valid, disabled, visible, what label to show. They run after state settles and write to a separate `_concerns` proxy, never to `state` itself.
|
|
223
135
|
|
|
224
136
|
```tsx
|
|
225
|
-
store.useConcerns('
|
|
226
|
-
//
|
|
137
|
+
store.useConcerns('checkout', {
|
|
138
|
+
// Zod validation
|
|
227
139
|
'user.email': {
|
|
140
|
+
validationState: { schema: z.string().email('Enter a valid email') },
|
|
228
141
|
disabledWhen: { boolLogic: { IS_EQUAL: ['status', 'submitted'] } },
|
|
142
|
+
dynamicTooltip: { template: 'Sending confirmation to {{user.email}}' },
|
|
229
143
|
},
|
|
230
144
|
|
|
231
|
-
//
|
|
145
|
+
// Card number: validate + show only when paying by card
|
|
232
146
|
'payment.cardNumber': {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
AND: [
|
|
236
|
-
{ IS_EQUAL: ['payment.method', 'card'] },
|
|
237
|
-
{ EXISTS: 'user.email' },
|
|
238
|
-
],
|
|
239
|
-
},
|
|
240
|
-
},
|
|
147
|
+
validationState: { schema: z.string().regex(/^\d{16}$/, 'Must be 16 digits') },
|
|
148
|
+
visibleWhen: { boolLogic: { IS_EQUAL: ['payment.method', 'card'] } },
|
|
241
149
|
},
|
|
242
150
|
|
|
243
|
-
//
|
|
151
|
+
// Lock order total above threshold
|
|
244
152
|
'order.total': {
|
|
245
153
|
readonlyWhen: { boolLogic: { GT: ['order.total', 10000] } },
|
|
246
154
|
},
|
|
247
155
|
|
|
248
|
-
//
|
|
249
|
-
'
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
],
|
|
156
|
+
// ValueLogic: label adapts to stock level
|
|
157
|
+
'product.quantity': {
|
|
158
|
+
dynamicLabel: {
|
|
159
|
+
valueLogic: {
|
|
160
|
+
IF: { LTE: ['product.quantity', 5] },
|
|
161
|
+
THEN: 'Quantity (low stock)',
|
|
162
|
+
ELSE: 'Quantity',
|
|
256
163
|
},
|
|
257
164
|
},
|
|
258
165
|
},
|
|
259
166
|
})
|
|
260
167
|
|
|
168
|
+
// Read all concerns for a field via useFieldStore
|
|
169
|
+
const { value, setValue, validationState, disabledWhen, dynamicTooltip } =
|
|
170
|
+
store.useFieldStore('user.email')
|
|
171
|
+
|
|
261
172
|
// Available BoolLogic operators:
|
|
262
173
|
// IS_EQUAL, EXISTS, IS_EMPTY, GT, LT, GTE, LTE, IN, AND, OR, NOT
|
|
263
174
|
```
|
|
264
175
|
|
|
265
176
|
## Side Effects
|
|
266
177
|
|
|
267
|
-
|
|
178
|
+
Side effects describe *what must change* when state changes — enforcing invariants, keeping related fields in sync, reacting to mutations. They run inside the pipeline and can write back to `state`.
|
|
268
179
|
|
|
269
180
|
```tsx
|
|
270
|
-
store.useSideEffects('
|
|
181
|
+
store.useSideEffects('checkout', {
|
|
182
|
+
// Sync billing ↔ shipping contact details bidirectionally
|
|
271
183
|
syncPaths: [
|
|
272
|
-
['billing.email', 'shipping.email'],
|
|
184
|
+
['billing.email', 'shipping.email'],
|
|
273
185
|
['billing.phone', 'shipping.phone'],
|
|
274
186
|
],
|
|
275
|
-
})
|
|
276
|
-
// When user types in billing.email, shipping.email updates automatically
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Flip Paths
|
|
280
187
|
|
|
281
|
-
|
|
282
|
-
store.useSideEffects('flips', {
|
|
188
|
+
// Express and standard shipping are mutually exclusive
|
|
283
189
|
flipPaths: [
|
|
284
|
-
['
|
|
285
|
-
['isExpanded', 'isCollapsed'],
|
|
190
|
+
['shipping.express', 'shipping.standard'],
|
|
286
191
|
],
|
|
287
|
-
})
|
|
288
|
-
```
|
|
289
192
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
```tsx
|
|
293
|
-
store.useSideEffects('agg', {
|
|
193
|
+
// Order total = consensus across items (undefined if they differ)
|
|
294
194
|
aggregations: [
|
|
295
|
-
|
|
296
|
-
['
|
|
297
|
-
['
|
|
298
|
-
['summary.price', 'legs.2.price'],
|
|
195
|
+
['order.total', 'items.0.price'],
|
|
196
|
+
['order.total', 'items.1.price'],
|
|
197
|
+
['order.total', 'items.2.price'],
|
|
299
198
|
],
|
|
300
|
-
})
|
|
301
|
-
// summary.price = leg price if ALL legs match, undefined if they differ
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Listeners
|
|
305
199
|
|
|
306
|
-
|
|
307
|
-
store.useSideEffects('listeners', {
|
|
200
|
+
// Stamp audit trail on any profile change
|
|
308
201
|
listeners: [
|
|
309
202
|
{
|
|
310
|
-
path: 'user
|
|
311
|
-
scope: 'user
|
|
312
|
-
fn: (
|
|
313
|
-
// changes: [['name', 'Alice', {}]] — paths relative to scope
|
|
314
|
-
// state: { name: 'Alice', email: '...' } — user.profile sub-object
|
|
315
|
-
return [['audit.lastEdit', Date.now(), {}]] // return FULL paths for new changes
|
|
316
|
-
},
|
|
203
|
+
path: 'user',
|
|
204
|
+
scope: 'user',
|
|
205
|
+
fn: (_changes, _state) => [['audit.lastModified', Date.now(), {}]],
|
|
317
206
|
},
|
|
318
207
|
],
|
|
319
208
|
})
|
|
320
209
|
```
|
|
321
210
|
|
|
322
|
-
##
|
|
211
|
+
## Reading and Writing State
|
|
323
212
|
|
|
324
213
|
```tsx
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const {
|
|
334
|
-
|
|
335
|
-
|
|
214
|
+
// useStore — simple [value, setter] tuple (like useState)
|
|
215
|
+
const [name, setName] = store.useStore('user.name')
|
|
216
|
+
|
|
217
|
+
// useFieldStore — object API with all concerns merged in
|
|
218
|
+
const { value, setValue, validationState, disabledWhen } =
|
|
219
|
+
store.useFieldStore('user.email')
|
|
220
|
+
|
|
221
|
+
// useJitStore — bulk updates and non-reactive reads
|
|
222
|
+
const { setChanges } = store.useJitStore()
|
|
223
|
+
setChanges([
|
|
224
|
+
['user.name', 'Alice', {}],
|
|
225
|
+
['user.email', 'alice@example.com', {}],
|
|
226
|
+
])
|
|
336
227
|
```
|
|
337
228
|
|
|
338
229
|
## Composable Field Hooks
|
|
@@ -341,44 +232,24 @@ const { dynamicTooltip, dynamicLabel } = store.useFieldStore('legs.0.strike')
|
|
|
341
232
|
import { useBufferedField, useThrottledField, useTransformedField } from '@sladg/apex-state'
|
|
342
233
|
|
|
343
234
|
// Buffer edits locally, commit/cancel explicitly
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return (
|
|
349
|
-
<div>
|
|
350
|
-
<input value={buffered.value} onChange={(e) => buffered.setValue(Number(e.target.value))} />
|
|
351
|
-
<button onClick={buffered.commit} disabled={!buffered.isDirty}>Save</button>
|
|
352
|
-
<button onClick={buffered.cancel}>Cancel</button>
|
|
353
|
-
</div>
|
|
354
|
-
)
|
|
355
|
-
}
|
|
235
|
+
const raw = store.useFieldStore('product.price')
|
|
236
|
+
const buffered = useBufferedField(raw)
|
|
237
|
+
// buffered.value, buffered.setValue, buffered.commit(), buffered.cancel(), buffered.isDirty
|
|
356
238
|
|
|
357
|
-
// Throttle rapid
|
|
358
|
-
const
|
|
359
|
-
const raw = store.useFieldStore('audio.volume')
|
|
360
|
-
const throttled = useThrottledField(raw, { ms: 100 })
|
|
361
|
-
return <input type="range" value={throttled.value} onChange={(e) => throttled.setValue(Number(e.target.value))} />
|
|
362
|
-
}
|
|
239
|
+
// Throttle rapid updates (e.g. range sliders)
|
|
240
|
+
const throttled = useThrottledField(raw, { ms: 100 })
|
|
363
241
|
|
|
364
|
-
// Transform display format (cents
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
from: (dollars: string) => Math.round(parseFloat(dollars) * 100), // display -> store
|
|
370
|
-
})
|
|
371
|
-
return <input value={formatted.value} onChange={(e) => formatted.setValue(e.target.value)} />
|
|
372
|
-
}
|
|
242
|
+
// Transform display format (cents ↔ dollars)
|
|
243
|
+
const formatted = useTransformedField(raw, {
|
|
244
|
+
to: (cents: number) => (cents / 100).toFixed(2),
|
|
245
|
+
from: (dollars: string) => Math.round(parseFloat(dollars) * 100),
|
|
246
|
+
})
|
|
373
247
|
|
|
374
|
-
// Chain
|
|
375
|
-
const
|
|
376
|
-
const buffered = useBufferedField(raw)
|
|
377
|
-
const display = useTransformedField(buffered, {
|
|
248
|
+
// Chain: buffered + transformed
|
|
249
|
+
const display = useTransformedField(useBufferedField(raw), {
|
|
378
250
|
to: (cents) => (cents / 100).toFixed(2),
|
|
379
251
|
from: (dollars) => Math.round(parseFloat(dollars) * 100),
|
|
380
252
|
})
|
|
381
|
-
// display has: value, setValue, commit, cancel, isDirty
|
|
382
253
|
```
|
|
383
254
|
|
|
384
255
|
## Hash Key Paths for Record Types
|
|
@@ -387,19 +258,19 @@ const display = useTransformedField(buffered, {
|
|
|
387
258
|
import { _ } from '@sladg/apex-state'
|
|
388
259
|
|
|
389
260
|
// _() marks a segment as a hash key for Record-typed paths
|
|
390
|
-
const
|
|
261
|
+
const itemPath = `catalog.categories.${_('c1')}.items`
|
|
391
262
|
// -> typed string containing HASH_KEY marker
|
|
392
263
|
|
|
393
264
|
// Use with concerns — applies to ALL keys in the Record
|
|
394
265
|
store.useConcerns('wildcards', {
|
|
395
|
-
[
|
|
266
|
+
[itemPath]: {
|
|
396
267
|
validationState: { schema: z.number().min(0) },
|
|
397
268
|
disabledWhen: { boolLogic: { IS_EQUAL: ['status', 'locked'] } },
|
|
398
269
|
},
|
|
399
270
|
})
|
|
400
271
|
|
|
401
272
|
// Multiple hash keys for deeply nested Records
|
|
402
|
-
const nestedPath = `
|
|
273
|
+
const nestedPath = `catalog.${_('c1')}.products.${_('p1')}.variants.${_('v1')}.price`
|
|
403
274
|
```
|
|
404
275
|
|
|
405
276
|
## Type-Safe Paths with Configurable Depth
|
|
@@ -512,14 +383,23 @@ it('tracks concern and side effect registrations', () => {
|
|
|
512
383
|
|
|
513
384
|
## Performance
|
|
514
385
|
|
|
515
|
-
|
|
386
|
+
Benchmarks run on Apple M4 Pro against a full e-commerce pipeline: 75 sync pairs, 40 flip pairs, 100 BoolLogic expressions, 85 listeners with real JS handlers.
|
|
516
387
|
|
|
517
|
-
|
|
|
518
|
-
|
|
519
|
-
| Single field
|
|
520
|
-
|
|
|
521
|
-
|
|
|
522
|
-
|
|
|
388
|
+
| Scenario | Changes | Duration |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| Single field change | 1 | 1.4µs |
|
|
391
|
+
| Order confirmation | 1 | 40µs |
|
|
392
|
+
| Dashboard aggregation (10 orders) | 10 | 295µs |
|
|
393
|
+
| Bulk price update (3 Record levels deep) | 60 | 7.2ms |
|
|
394
|
+
| Full catalog refresh (everything fires) | 135 | 7.4ms |
|
|
395
|
+
|
|
396
|
+
Typical interactions are sub-millisecond. The stress case — 135 changes triggering cascading sync, flip, BoolLogic, and 85 listener callbacks across 3 Record levels — completes in **7.4ms**.
|
|
397
|
+
|
|
398
|
+
**Bundle:** ~105 KB JS + ~697 KB WASM, loaded separately and cached by the browser. For eager loading:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import '@sladg/apex-state/preload'
|
|
402
|
+
```
|
|
523
403
|
|
|
524
404
|
See [Benchmark Comparison](docs/BENCHMARK_COMPARISON.md) for the full analysis.
|
|
525
405
|
|
|
@@ -562,21 +442,32 @@ See [Benchmark Comparison](docs/BENCHMARK_COMPARISON.md) for the full analysis.
|
|
|
562
442
|
setValue("email", "alice@example.com")
|
|
563
443
|
|
|
|
564
444
|
v
|
|
565
|
-
[WASM/Rust]
|
|
445
|
+
[WASM/Rust] single processChanges() call:
|
|
446
|
+
- shadow state update
|
|
447
|
+
- aggregation → sync → flip
|
|
448
|
+
- expression evaluation (BoolLogic + ValueLogic, unified)
|
|
449
|
+
- listener waves + validator dispatch (via externref — no JS round-trips)
|
|
566
450
|
|
|
|
567
451
|
v
|
|
568
|
-
[JS]
|
|
452
|
+
[JS] partition result changes by meta flag (state vs _concerns)
|
|
569
453
|
|
|
|
570
|
-
|
|
571
|
-
|
|
454
|
+
├── applyBatch(stateChanges) → valtio state proxy
|
|
455
|
+
└── applyConcernChanges(concernChanges) → valtio _concerns proxy
|
|
572
456
|
|
|
|
573
457
|
v
|
|
574
|
-
|
|
458
|
+
React re-render
|
|
575
459
|
```
|
|
576
460
|
|
|
577
|
-
**Dual-layer design:** JS/React owns reactivity and rendering. Rust/WASM owns
|
|
461
|
+
**Dual-layer design:** JS/React owns reactivity and rendering. Rust/WASM owns the full pipeline — shadow state, sync/flip graphs, expression evaluation, and listener dispatch (via externref callbacks). The JS layer normalizes inputs, calls one WASM function, and applies the result. The boundary is thin: paths as strings, values as JSON.
|
|
462
|
+
|
|
463
|
+
**Side effects vs. concerns:** These are two distinct systems with different authority and different timing.
|
|
464
|
+
|
|
465
|
+
- **Side effects** model *causality* — because X changed, Y must change. They enforce invariants in the data model (sync, flip, aggregate, listen). They run *during* the pipeline and can write to `state`.
|
|
466
|
+
- **Concerns** model *interpretation* — given settled state, what does this field mean for the UI? They annotate fields with metadata (valid, disabled, visible, label). They run *after* the pipeline and can only write to `_concerns`.
|
|
467
|
+
|
|
468
|
+
Both use the same expression language (`BoolLogic`, `ValueLogic`) — that's just a shared way to write declarative conditions. What those conditions *drive* is determined by the registration context, not the expression itself.
|
|
578
469
|
|
|
579
|
-
See [WASM Architecture](docs/WASM_ARCHITECTURE.md) for the full specification.
|
|
470
|
+
See [Architecture Guide](docs/guides/ARCHITECTURE.md) and [WASM Architecture](docs/WASM_ARCHITECTURE.md) for the full specification.
|
|
580
471
|
|
|
581
472
|
## Development
|
|
582
473
|
|
|
@@ -609,7 +500,7 @@ cargo install wasm-pack
|
|
|
609
500
|
| [Wildcard Paths](docs/WILD_FUNCTION_GUIDE.md) | `_()` hash key utility for Record types |
|
|
610
501
|
| [String Interpolation](docs/INTERPOLATION.md) | Template helpers for dynamic text concerns |
|
|
611
502
|
| [Testing Mock](docs/TESTING_MOCK.md) | Mock module for consumer tests (`vi.mock`) |
|
|
612
|
-
| [Debug
|
|
503
|
+
| [Debug Logging](docs/DEBUG_LOGGING.md) | Pipeline trace, console output, debug configuration |
|
|
613
504
|
| [Full Index](docs/README.md) | Complete documentation index |
|
|
614
505
|
|
|
615
506
|
## Roadmap
|