machinalayout 0.1.0 → 0.3.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 +295 -49
- package/dist/chunk-2ZQ2RFFI.js +400 -0
- package/dist/chunk-33CKBEJH.js +186 -0
- package/dist/chunk-BJOQRPPX.js +382 -0
- package/dist/chunk-KYWOCAHK.js +205 -0
- package/dist/chunk-RJYRJ3LD.js +0 -0
- package/dist/chunk-SVWYWI7I.js +59 -0
- package/dist/chunk-VREK57S3.js +13 -0
- package/dist/chunk-ZVDE7PX4.js +222 -0
- package/dist/debugOverlay-pJpj0n5H.d.ts +125 -0
- package/dist/deus/index.d.ts +14 -0
- package/dist/deus/index.js +26 -0
- package/dist/dispatch/index.d.ts +49 -0
- package/dist/dispatch/index.js +217 -0
- package/dist/handoff/index.d.ts +44 -0
- package/dist/handoff/index.js +83 -0
- package/dist/index.d.ts +54 -236
- package/dist/index.js +753 -583
- package/dist/inspect/index.d.ts +8 -0
- package/dist/inspect/index.js +97 -0
- package/dist/react/index.d.ts +41 -0
- package/dist/react/index.js +9 -0
- package/dist/react-native/index.d.ts +30 -0
- package/dist/react-native/index.js +84 -0
- package/dist/screenCatalog-ZjonGiOi.d.ts +46 -0
- package/dist/text/index.d.ts +10 -0
- package/dist/text/index.js +9 -0
- package/dist/text/react/index.d.ts +14 -0
- package/dist/text/react/index.js +7 -0
- package/dist/text/react-native/index.d.ts +16 -0
- package/dist/text/react-native/index.js +155 -0
- package/dist/text/vue/index.d.ts +113 -0
- package/dist/text/vue/index.js +202 -0
- package/dist/types-B90jb3RW.d.ts +184 -0
- package/dist/types-C4poVJpR.d.ts +74 -0
- package/dist/types-DLYAhNXw.d.ts +32 -0
- package/dist/vue/index.d.ts +173 -0
- package/dist/vue/index.js +112 -0
- package/docs/adapter-packaging-a0-plan.md +352 -0
- package/docs/adapters.md +19 -0
- package/docs/api-coherence-m8-audit.md +397 -0
- package/docs/deusmachina.md +108 -0
- package/docs/error-codes.md +95 -0
- package/docs/grid-arrange-m5a-contract.md +480 -0
- package/docs/grid-arrange.md +51 -0
- package/docs/inspection-and-handoff.md +126 -0
- package/docs/layout-interpolation.md +52 -0
- package/docs/machina-dispatch-d0-contract.md +496 -0
- package/docs/machina-dispatch.md +143 -0
- package/docs/named-layers.md +40 -0
- package/docs/react-adapter.md +63 -58
- package/docs/react-native-adapter.md +56 -0
- package/docs/react-native-text-renderer.md +50 -0
- package/docs/reference-alignment-m7a-contract.md +384 -0
- package/docs/reference-alignment.md +44 -0
- package/docs/responsive-variants.md +54 -0
- package/docs/screen-catalog-and-viewports.md +124 -0
- package/docs/stack-geometry-helpers.md +115 -0
- package/docs/vue-adapter.md +55 -0
- package/docs/vue-text-renderer.md +55 -0
- package/package.json +127 -60
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
> D1 status (2026-05-12): Implemented in `src/dispatch` with subpath export `machinalayout/dispatch`.
|
|
2
|
+
|
|
3
|
+
# MachinaDispatch D0 Contract
|
|
4
|
+
|
|
5
|
+
## 1. Executive summary
|
|
6
|
+
|
|
7
|
+
MachinaDispatch D0 defines a tiny, pure, framework-independent **event dispatch table** module for table-driven UI transitions.
|
|
8
|
+
|
|
9
|
+
It is intentionally narrow:
|
|
10
|
+
|
|
11
|
+
- Input: `state + event + dispatch tables`.
|
|
12
|
+
- Output: `next state`.
|
|
13
|
+
- Canonical operation: `dispatchEvent(state, event, tables)`.
|
|
14
|
+
|
|
15
|
+
It is **not** a router, store, middleware layer, effect engine, or state framework. It only maps events to one-field state updates through compact **columnar dispatch tables**.
|
|
16
|
+
|
|
17
|
+
## 2. Actual problem
|
|
18
|
+
|
|
19
|
+
Many UI transitions are repetitive and local, such as:
|
|
20
|
+
|
|
21
|
+
- set a route-like field,
|
|
22
|
+
- toggle a boolean,
|
|
23
|
+
- increment/decrement a number,
|
|
24
|
+
- derive a field from an event suffix.
|
|
25
|
+
|
|
26
|
+
In practice, these flows often begin as repeated event `if`/`switch` chains. The Oct Storefront M1→M7 evolution showed that these patterns become clearer and safer when represented as data tables and pure reducers.
|
|
27
|
+
|
|
28
|
+
D0 captures that lesson: repeated UI event logic should become data, while remaining a tiny pure dispatcher.
|
|
29
|
+
|
|
30
|
+
## 3. Non-goals
|
|
31
|
+
|
|
32
|
+
D1 must explicitly exclude:
|
|
33
|
+
|
|
34
|
+
- React hook,
|
|
35
|
+
- Vue composable,
|
|
36
|
+
- React Native hook/composable,
|
|
37
|
+
- global store,
|
|
38
|
+
- subscriptions,
|
|
39
|
+
- middleware,
|
|
40
|
+
- async actions/loaders,
|
|
41
|
+
- browser history ownership,
|
|
42
|
+
- URL parser,
|
|
43
|
+
- nested routes,
|
|
44
|
+
- guards,
|
|
45
|
+
- effects,
|
|
46
|
+
- undo/redo,
|
|
47
|
+
- persistence,
|
|
48
|
+
- devtools,
|
|
49
|
+
- reducer composition framework,
|
|
50
|
+
- Dominatus/Octomata-style state machine system,
|
|
51
|
+
- router cathedral behavior.
|
|
52
|
+
|
|
53
|
+
## 4. Design principles
|
|
54
|
+
|
|
55
|
+
1. Pure functions only.
|
|
56
|
+
2. No framework dependency.
|
|
57
|
+
3. No React/Vue/RN hooks in D1.
|
|
58
|
+
4. No subscriptions.
|
|
59
|
+
5. No global store.
|
|
60
|
+
6. No browser history.
|
|
61
|
+
7. No async loaders/actions.
|
|
62
|
+
8. No middleware.
|
|
63
|
+
9. No guards.
|
|
64
|
+
10. No nested routes.
|
|
65
|
+
11. No effects.
|
|
66
|
+
12. No state machine.
|
|
67
|
+
13. No Dominatus port.
|
|
68
|
+
14. No router cathedral.
|
|
69
|
+
|
|
70
|
+
Operationally:
|
|
71
|
+
|
|
72
|
+
- If no rule matches, return the original state object by reference.
|
|
73
|
+
- If one rule matches and produces a changed value, return a shallow copy with one field updated.
|
|
74
|
+
- Never mutate input state or tables.
|
|
75
|
+
|
|
76
|
+
## 5. Columnar table model
|
|
77
|
+
|
|
78
|
+
Canonical authoring model is **columnar operation-specific tables**, not repeated row objects.
|
|
79
|
+
|
|
80
|
+
Rationale:
|
|
81
|
+
|
|
82
|
+
- Reduces repeated keys (`event`, `field`, `op`) in small tables.
|
|
83
|
+
- Keeps intent close to spreadsheet-like authoring.
|
|
84
|
+
- Improves scanability for simple UI transition rules.
|
|
85
|
+
|
|
86
|
+
Canonical shape:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const dispatch = defineDispatchTables<AppState>({
|
|
90
|
+
set: {
|
|
91
|
+
events: ["nav.home", "nav.settings"],
|
|
92
|
+
fields: ["route", "route"],
|
|
93
|
+
values: ["home", "settings"],
|
|
94
|
+
},
|
|
95
|
+
toggle: {
|
|
96
|
+
events: ["filter.new"],
|
|
97
|
+
fields: ["newOnly"],
|
|
98
|
+
},
|
|
99
|
+
increment: {
|
|
100
|
+
events: ["cart.add", "cart.remove"],
|
|
101
|
+
fields: ["cartCount", "cartCount"],
|
|
102
|
+
by: [1, -1],
|
|
103
|
+
},
|
|
104
|
+
setSuffix: {
|
|
105
|
+
prefixes: ["product.inspect."],
|
|
106
|
+
fields: ["selectedProduct"],
|
|
107
|
+
allowedSuffixes: [PRODUCT_IDS],
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Table “views”:
|
|
113
|
+
|
|
114
|
+
- `set`: `event | field | value`
|
|
115
|
+
- `toggle`: `event | field`
|
|
116
|
+
- `increment`: `event | field | by`
|
|
117
|
+
- `setSuffix`: `prefix | field | allowedSuffixes`
|
|
118
|
+
- `incrementSuffix`: `prefix | field | by | allowedSuffixes`
|
|
119
|
+
|
|
120
|
+
Naming decision:
|
|
121
|
+
|
|
122
|
+
- Use plural column names (`events`, `fields`, `values`, `prefixes`) because each property is a column vector.
|
|
123
|
+
|
|
124
|
+
## 6. Type proposal
|
|
125
|
+
|
|
126
|
+
D1 should start from this model (with small refinements):
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
export type SetDispatchTable<TState> = {
|
|
130
|
+
events: readonly string[];
|
|
131
|
+
fields: readonly (keyof TState)[];
|
|
132
|
+
values: readonly unknown[];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type ToggleDispatchTable<TState> = {
|
|
136
|
+
events: readonly string[];
|
|
137
|
+
fields: readonly (keyof TState)[];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type IncrementDispatchTable<TState> = {
|
|
141
|
+
events: readonly string[];
|
|
142
|
+
fields: readonly (keyof TState)[];
|
|
143
|
+
by?: readonly number[];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type PrefixSetDispatchTable<TState> = {
|
|
147
|
+
prefixes: readonly string[];
|
|
148
|
+
fields: readonly (keyof TState)[];
|
|
149
|
+
allowedSuffixes?: readonly (readonly string[] | undefined)[];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type PrefixIncrementDispatchTable<TState> = {
|
|
153
|
+
prefixes: readonly string[];
|
|
154
|
+
fields: readonly (keyof TState)[];
|
|
155
|
+
by?: readonly number[];
|
|
156
|
+
allowedSuffixes?: readonly (readonly string[] | undefined)[];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export type MachinaDispatchTables<TState> = {
|
|
160
|
+
set?: SetDispatchTable<TState>;
|
|
161
|
+
toggle?: ToggleDispatchTable<TState>;
|
|
162
|
+
increment?: IncrementDispatchTable<TState>;
|
|
163
|
+
setSuffix?: PrefixSetDispatchTable<TState>;
|
|
164
|
+
incrementSuffix?: PrefixIncrementDispatchTable<TState>;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export function defineDispatchTables<TState>(
|
|
168
|
+
tables: MachinaDispatchTables<TState>,
|
|
169
|
+
): MachinaDispatchTables<TState>;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
D0 decisions:
|
|
173
|
+
|
|
174
|
+
- `by` defaults to `1` when omitted per row.
|
|
175
|
+
- `allowedSuffixes` is per-row (`index` aligned with `prefixes`/`fields`).
|
|
176
|
+
- Suffix mismatch against an allowed list is a **non-match**, not an error.
|
|
177
|
+
- `incrementSuffix` is included in D1 (simple and useful for catalog-driven count updates).
|
|
178
|
+
- `defineDispatchTables` should preserve type inference and perform optional lightweight runtime table-shape validation in development; `dispatchEvent` must still validate defensively.
|
|
179
|
+
|
|
180
|
+
Type limits acknowledged:
|
|
181
|
+
|
|
182
|
+
- `keyof TState` does not guarantee runtime value type at a field.
|
|
183
|
+
- `toggle` and `increment` require runtime value checks.
|
|
184
|
+
- `values` remain `unknown[]` in D1 for simplicity.
|
|
185
|
+
- Advanced field-aware type gymnastics are deferred.
|
|
186
|
+
|
|
187
|
+
## 7. Dispatch semantics
|
|
188
|
+
|
|
189
|
+
Primary function for D1:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
dispatchEvent<TState extends Record<string, unknown>>(
|
|
193
|
+
state: TState,
|
|
194
|
+
event: string,
|
|
195
|
+
tables: MachinaDispatchTables<TState>,
|
|
196
|
+
): TState
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Matching order is fixed:
|
|
200
|
+
|
|
201
|
+
1. `set`
|
|
202
|
+
2. `toggle`
|
|
203
|
+
3. `increment`
|
|
204
|
+
4. `setSuffix`
|
|
205
|
+
5. `incrementSuffix`
|
|
206
|
+
|
|
207
|
+
Rules:
|
|
208
|
+
|
|
209
|
+
- Within a group, scan arrays in index order.
|
|
210
|
+
- First matching row in a group wins.
|
|
211
|
+
- First matching group by fixed order wins.
|
|
212
|
+
- No multi-action event fan-out.
|
|
213
|
+
- No global explicit priority system in D1.
|
|
214
|
+
|
|
215
|
+
Result behavior:
|
|
216
|
+
|
|
217
|
+
- If no match: return original `state` reference.
|
|
218
|
+
- If match and computed value is `Object.is(currentValue, nextValue)`: return original `state` reference.
|
|
219
|
+
- Else: return shallow copied state with only the target field updated.
|
|
220
|
+
|
|
221
|
+
Helper decision for D1:
|
|
222
|
+
|
|
223
|
+
- Include internal/public helpers if they reduce duplication and clarify behavior:
|
|
224
|
+
- `resolveEventValue<TValue>(event, { events, values })`
|
|
225
|
+
- `matchEventPrefix(event, prefix, allowedSuffixes?)`
|
|
226
|
+
- These helpers are acceptable D1 exports from `machinalayout/dispatch` if kept narrow and pure.
|
|
227
|
+
|
|
228
|
+
## 8. Operation semantics
|
|
229
|
+
|
|
230
|
+
### `set`
|
|
231
|
+
|
|
232
|
+
- Match: exact event string equality.
|
|
233
|
+
- Update: `next[field] = value`.
|
|
234
|
+
|
|
235
|
+
### `toggle`
|
|
236
|
+
|
|
237
|
+
- Match: exact event equality.
|
|
238
|
+
- Runtime requirement: current field value is boolean.
|
|
239
|
+
- Update: `next[field] = !state[field]`.
|
|
240
|
+
- Invalid runtime value => throw dispatch error.
|
|
241
|
+
|
|
242
|
+
### `increment`
|
|
243
|
+
|
|
244
|
+
- Match: exact event equality.
|
|
245
|
+
- Runtime requirement: current field value is number.
|
|
246
|
+
- Delta `by`:
|
|
247
|
+
- default `1` if `by` omitted,
|
|
248
|
+
- else use row delta,
|
|
249
|
+
- must be finite.
|
|
250
|
+
- Update: `next[field] = state[field] + by`.
|
|
251
|
+
- Invalid field value or invalid `by` => throw dispatch error.
|
|
252
|
+
|
|
253
|
+
### `setSuffix`
|
|
254
|
+
|
|
255
|
+
- Match: event starts with row `prefix`.
|
|
256
|
+
- `suffix = event.slice(prefix.length)`.
|
|
257
|
+
- If row allowed list exists: suffix must be present, else non-match.
|
|
258
|
+
- Update: `next[field] = suffix`.
|
|
259
|
+
|
|
260
|
+
### `incrementSuffix`
|
|
261
|
+
|
|
262
|
+
- Match: event starts with row `prefix`.
|
|
263
|
+
- Suffix handling same as `setSuffix`.
|
|
264
|
+
- Runtime requirement: target field is number.
|
|
265
|
+
- Delta `by` default is `1`, row override when provided and finite.
|
|
266
|
+
- Update: numeric increment.
|
|
267
|
+
- Included in D1.
|
|
268
|
+
|
|
269
|
+
## 9. Error model
|
|
270
|
+
|
|
271
|
+
D1 should define a dedicated error type, separate from layout errors:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
export type MachinaDispatchErrorCode =
|
|
275
|
+
| "InvalidDispatchTable"
|
|
276
|
+
| "InvalidDispatchField"
|
|
277
|
+
| "InvalidDispatchValue"
|
|
278
|
+
| "InvalidDispatchEvent";
|
|
279
|
+
|
|
280
|
+
export class MachinaDispatchError extends Error {
|
|
281
|
+
readonly code: MachinaDispatchErrorCode;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Throw conditions:
|
|
286
|
+
|
|
287
|
+
- `InvalidDispatchTable`
|
|
288
|
+
- column length mismatch within a table group,
|
|
289
|
+
- structurally invalid table columns at runtime.
|
|
290
|
+
- `InvalidDispatchField`
|
|
291
|
+
- referenced field key is missing in runtime state object.
|
|
292
|
+
- `InvalidDispatchValue`
|
|
293
|
+
- toggle target is not boolean,
|
|
294
|
+
- increment target is not number,
|
|
295
|
+
- increment delta is non-finite.
|
|
296
|
+
- `InvalidDispatchEvent`
|
|
297
|
+
- non-string event input,
|
|
298
|
+
- non-string prefix/event entries in table misuse.
|
|
299
|
+
|
|
300
|
+
Suffix not in `allowedSuffixes` is non-match (not an error).
|
|
301
|
+
|
|
302
|
+
No diagnostics subsystem is required in D1.
|
|
303
|
+
|
|
304
|
+
## 10. Immutability semantics
|
|
305
|
+
|
|
306
|
+
D1 contract:
|
|
307
|
+
|
|
308
|
+
- Never mutate `state`.
|
|
309
|
+
- Never mutate `tables`.
|
|
310
|
+
- No match => return exact same state reference.
|
|
311
|
+
- Match with identity-equal value via `Object.is` => return same state reference.
|
|
312
|
+
- Match with changed value => return new shallow object with one updated field.
|
|
313
|
+
|
|
314
|
+
This minimizes unnecessary UI rerenders in userland frameworks.
|
|
315
|
+
|
|
316
|
+
## 11. Relationship to routing
|
|
317
|
+
|
|
318
|
+
Routing-like behavior is represented as state assignment:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
type AppState = { route: "library" | "settings" };
|
|
322
|
+
|
|
323
|
+
const tables = defineDispatchTables<AppState>({
|
|
324
|
+
set: {
|
|
325
|
+
events: ["nav.library", "nav.settings"],
|
|
326
|
+
fields: ["route", "route"],
|
|
327
|
+
values: ["library", "settings"],
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const next = dispatchEvent(state, "nav.settings", tables);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
No browser history, nested route tree, loader/action graph, or guard engine is owned by MachinaDispatch.
|
|
335
|
+
|
|
336
|
+
## 12. Relationship to MachinaLayout
|
|
337
|
+
|
|
338
|
+
Composition without coupling:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
const nextState = dispatchEvent(state, event, dispatch);
|
|
342
|
+
const routeRows = rowsByRoute[nextState.route];
|
|
343
|
+
const layout = resolveLayoutRows(routeRows, rootRect);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Boundaries:
|
|
347
|
+
|
|
348
|
+
- MachinaDispatch does not call layout resolvers.
|
|
349
|
+
- MachinaLayout does not call dispatch functions.
|
|
350
|
+
- Userland composes both.
|
|
351
|
+
|
|
352
|
+
## 13. Package/subpath plan
|
|
353
|
+
|
|
354
|
+
Recommended packaging target for D1:
|
|
355
|
+
|
|
356
|
+
- public subpath: `machinalayout/dispatch`
|
|
357
|
+
- build outputs:
|
|
358
|
+
- `dist/dispatch/index.js`
|
|
359
|
+
- `dist/dispatch/index.d.ts`
|
|
360
|
+
|
|
361
|
+
Policy:
|
|
362
|
+
|
|
363
|
+
- no framework peer dependencies,
|
|
364
|
+
- no adapter dependencies,
|
|
365
|
+
- subpath-only initially to avoid root API bloat.
|
|
366
|
+
|
|
367
|
+
## 14. Examples
|
|
368
|
+
|
|
369
|
+
### A) Routing as state assignment
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
const tables = defineDispatchTables<{ route: "home" | "settings" }>({
|
|
373
|
+
set: {
|
|
374
|
+
events: ["nav.home", "nav.settings"],
|
|
375
|
+
fields: ["route", "route"],
|
|
376
|
+
values: ["home", "settings"],
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### B) Filter toggle
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const tables = defineDispatchTables<{ newOnly: boolean }>({
|
|
385
|
+
toggle: {
|
|
386
|
+
events: ["filter.new"],
|
|
387
|
+
fields: ["newOnly"],
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### C) Cart increment/decrement
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
const tables = defineDispatchTables<{ cartCount: number }>({
|
|
396
|
+
increment: {
|
|
397
|
+
events: ["cart.add", "cart.remove"],
|
|
398
|
+
fields: ["cartCount", "cartCount"],
|
|
399
|
+
by: [1, -1],
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### D) Product inspect via prefix/suffix
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const PRODUCT_IDS = ["p1", "p2", "p3"] as const;
|
|
408
|
+
|
|
409
|
+
const tables = defineDispatchTables<{ selectedProduct: string | null }>({
|
|
410
|
+
setSuffix: {
|
|
411
|
+
prefixes: ["product.inspect."],
|
|
412
|
+
fields: ["selectedProduct"],
|
|
413
|
+
allowedSuffixes: [PRODUCT_IDS],
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### E) React userland usage (no custom hook)
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
const [state, setState] = useState(initialState);
|
|
422
|
+
|
|
423
|
+
function onEvent(event: string) {
|
|
424
|
+
setState((s) => dispatchEvent(s, event, tables));
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### F) Vue userland usage (no Pinia required)
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const state = ref(initialState);
|
|
432
|
+
|
|
433
|
+
function onEvent(event: string) {
|
|
434
|
+
state.value = dispatchEvent(state.value, event, tables);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### G) Integrating route state with Machina layout rows
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const next = dispatchEvent(state, event, tables);
|
|
442
|
+
const rows = rowsByRoute[next.route];
|
|
443
|
+
const layout = resolveLayoutRows(rows, rootRect);
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## 15. D1 implementation plan
|
|
447
|
+
|
|
448
|
+
1. Add `src/dispatch/types.ts`.
|
|
449
|
+
2. Add `src/dispatch/errors.ts` with `MachinaDispatchError` and codes.
|
|
450
|
+
3. Add `src/dispatch/dispatchEvent.ts`.
|
|
451
|
+
4. Add `src/dispatch/helpers.ts` (for `resolveEventValue`, `matchEventPrefix`) if included publicly.
|
|
452
|
+
5. Add `src/dispatch/index.ts`.
|
|
453
|
+
6. Add package subpath export `./dispatch` and build entry `dispatch/index`.
|
|
454
|
+
7. Add tests for dispatch semantics and error behavior.
|
|
455
|
+
8. Add docs/README section linking `machinalayout/dispatch`.
|
|
456
|
+
9. Run build/test/pack smoke checks.
|
|
457
|
+
|
|
458
|
+
## 16. D1 test plan
|
|
459
|
+
|
|
460
|
+
Planned coverage:
|
|
461
|
+
|
|
462
|
+
- set dispatch,
|
|
463
|
+
- toggle dispatch,
|
|
464
|
+
- increment dispatch,
|
|
465
|
+
- increment default `by`,
|
|
466
|
+
- increment row-specific `by`,
|
|
467
|
+
- setSuffix with allowed suffix,
|
|
468
|
+
- setSuffix with disallowed suffix returns same object,
|
|
469
|
+
- incrementSuffix behavior,
|
|
470
|
+
- fixed group order first-match behavior,
|
|
471
|
+
- first row wins within group,
|
|
472
|
+
- no match returns same object reference,
|
|
473
|
+
- matched identical value returns same object reference,
|
|
474
|
+
- matched changed value returns shallow copy,
|
|
475
|
+
- tables not mutated,
|
|
476
|
+
- table length mismatch error,
|
|
477
|
+
- invalid toggle field type error,
|
|
478
|
+
- invalid increment field type error,
|
|
479
|
+
- non-finite `by` error,
|
|
480
|
+
- missing runtime field error,
|
|
481
|
+
- route assignment scenario,
|
|
482
|
+
- React-style usage as plain function test,
|
|
483
|
+
- Vue-style usage as plain function test.
|
|
484
|
+
|
|
485
|
+
## 17. Risks and mitigations
|
|
486
|
+
|
|
487
|
+
- Risk: scope creep into state framework.
|
|
488
|
+
- Mitigation: enforce non-goals and pure dispatch-only API.
|
|
489
|
+
- Risk: confusion about routing ownership.
|
|
490
|
+
- Mitigation: explicitly document “routing is state assignment”; no history ownership.
|
|
491
|
+
- Risk: runtime type mismatch despite TS keys.
|
|
492
|
+
- Mitigation: strict runtime checks + explicit dispatch error codes.
|
|
493
|
+
- Risk: table authoring mistakes (length mismatch).
|
|
494
|
+
- Mitigation: deterministic validation and early `InvalidDispatchTable` errors.
|
|
495
|
+
- Risk: API bloat at package root.
|
|
496
|
+
- Mitigation: ship as `machinalayout/dispatch` subpath first.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# MachinaDispatch Runtime Guide (D1)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
MachinaDispatch is a tiny, pure table-driven event dispatcher for single-field state transitions:
|
|
6
|
+
|
|
7
|
+
`state + event + dispatch tables -> next state`
|
|
8
|
+
|
|
9
|
+
Import from the subpath:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineDispatchTables, dispatchEvent } from "machinalayout/dispatch";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Thesis
|
|
16
|
+
|
|
17
|
+
MachinaDispatch is not a router, store, middleware layer, or async framework. It only maps event strings to deterministic state updates through columnar tables.
|
|
18
|
+
|
|
19
|
+
## Dispatch tables
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
type AppState = { route: "home" | "settings"; cartCount: number; newOnly: boolean; selectedProduct: string };
|
|
23
|
+
|
|
24
|
+
const tables = defineDispatchTables<AppState>({
|
|
25
|
+
set: {
|
|
26
|
+
events: ["nav.home", "nav.settings"],
|
|
27
|
+
fields: ["route", "route"],
|
|
28
|
+
values: ["home", "settings"],
|
|
29
|
+
},
|
|
30
|
+
toggle: {
|
|
31
|
+
events: ["filter.new"],
|
|
32
|
+
fields: ["newOnly"],
|
|
33
|
+
},
|
|
34
|
+
increment: {
|
|
35
|
+
events: ["cart.add"],
|
|
36
|
+
fields: ["cartCount"],
|
|
37
|
+
by: [1],
|
|
38
|
+
},
|
|
39
|
+
setSuffix: {
|
|
40
|
+
prefixes: ["product.inspect."],
|
|
41
|
+
fields: ["selectedProduct"],
|
|
42
|
+
allowedSuffixes: [["p1", "p2"]],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Operation semantics
|
|
48
|
+
|
|
49
|
+
Matching order is fixed: `set -> toggle -> increment -> setSuffix -> incrementSuffix`.
|
|
50
|
+
|
|
51
|
+
- First matching group wins.
|
|
52
|
+
- Within each group, first matching row wins.
|
|
53
|
+
- No match returns the same state object.
|
|
54
|
+
|
|
55
|
+
## Error model
|
|
56
|
+
|
|
57
|
+
Errors throw `MachinaDispatchError` with stable codes:
|
|
58
|
+
|
|
59
|
+
- `InvalidDispatchTable`
|
|
60
|
+
- `InvalidDispatchField`
|
|
61
|
+
- `InvalidDispatchValue`
|
|
62
|
+
- `InvalidDispatchEvent`
|
|
63
|
+
|
|
64
|
+
## Immutability
|
|
65
|
+
|
|
66
|
+
- Never mutates input state or tables.
|
|
67
|
+
- Identity-equal updates return the original state reference.
|
|
68
|
+
- Changed updates return a shallow copy with one changed field.
|
|
69
|
+
|
|
70
|
+
## Routing as state assignment
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const next = dispatchEvent({ route: "home" }, "nav.settings", tables);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This is state assignment, not URL parsing/history/router trees.
|
|
77
|
+
|
|
78
|
+
## Composition with MachinaLayout
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const nextState = dispatchEvent(state, event, tables);
|
|
82
|
+
const rows = rowsByRoute[nextState.route];
|
|
83
|
+
const layout = resolveLayoutRows(rows, rootRect);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Dispatch and layout remain decoupled.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Smallest useful example
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
type CounterState = {
|
|
93
|
+
count: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const DISPATCH = defineDispatchTables<CounterState>({
|
|
97
|
+
increment: {
|
|
98
|
+
events: ["counter.increment"],
|
|
99
|
+
fields: ["count"],
|
|
100
|
+
by: [1],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const next = dispatchEvent({ count: 0 }, "counter.increment", DISPATCH);
|
|
105
|
+
// => { count: 1 }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
React usage needs only framework state:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const [state, setState] = useState<CounterState>({ count: 0 });
|
|
112
|
+
const send = (event: string) => {
|
|
113
|
+
setState((s) => dispatchEvent(s, event, DISPATCH));
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Vue usage is equally small:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const state = ref<CounterState>({ count: 0 });
|
|
121
|
+
const send = (event: string) => {
|
|
122
|
+
state.value = dispatchEvent(state.value, event, DISPATCH);
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
No MachinaDispatch hook, provider, router, or store runtime is required.
|
|
127
|
+
|
|
128
|
+
## When to use MachinaDispatch
|
|
129
|
+
|
|
130
|
+
Use MachinaDispatch when an event can be expressed as a simple field transition:
|
|
131
|
+
|
|
132
|
+
- `field = value`
|
|
133
|
+
- `field = !field`
|
|
134
|
+
- `field += n`
|
|
135
|
+
- `field = event suffix`
|
|
136
|
+
|
|
137
|
+
If behavior needs async effects, timers, retries, guards, hierarchical states, trace/replay, or orchestration, keep that in real app logic (or Dominatus/userland code) and keep MachinaDispatch as the tiny deterministic table layer.
|
|
138
|
+
|
|
139
|
+
If Dominatus would be overkill, you probably need a table, not a state manager.
|
|
140
|
+
|
|
141
|
+
## Non-goals
|
|
142
|
+
|
|
143
|
+
No hooks, composables, browser history, URL parsing, router trees, middleware, subscriptions, async actions/loaders, or global state runtime.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Named Layers (M6a)
|
|
2
|
+
|
|
3
|
+
Named layers add semantic paint grouping on top of existing node `z`.
|
|
4
|
+
|
|
5
|
+
## What layers do
|
|
6
|
+
- `layer` is optional node metadata.
|
|
7
|
+
- Layers affect paint order only.
|
|
8
|
+
- Geometry, placement, and coordinate spaces are unchanged.
|
|
9
|
+
|
|
10
|
+
## Layer and z bounds
|
|
11
|
+
- Layer registry z values are conceptually bounded to integer `-5..5`.
|
|
12
|
+
- Node `z` remains integer `-5..5`.
|
|
13
|
+
|
|
14
|
+
## Paint order
|
|
15
|
+
Siblings are painted by:
|
|
16
|
+
1. layer z ascending
|
|
17
|
+
2. node z ascending
|
|
18
|
+
3. original sibling order ascending
|
|
19
|
+
|
|
20
|
+
React CSS representation uses `zIndex = layerZ * 100 + nodeZ`.
|
|
21
|
+
|
|
22
|
+
## React adapter props
|
|
23
|
+
- `layers?: Record<string, { z: number }>`
|
|
24
|
+
- `defaultLayer?: string` (default: `"base"`)
|
|
25
|
+
|
|
26
|
+
If `layers` is omitted, behavior is equivalent to:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
{ base: { z: 0 } }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Fallback behavior
|
|
33
|
+
- Unknown node layer name: keeps declared name for metadata, layer z falls back to `0`.
|
|
34
|
+
- Invalid registry z (non-finite, non-integer, out of range): falls back to `0`.
|
|
35
|
+
|
|
36
|
+
## Important M6a limitations
|
|
37
|
+
- Not portals.
|
|
38
|
+
- No DOM reparenting.
|
|
39
|
+
- No root overlay container.
|
|
40
|
+
- No clipping escape. Overflow clipping from ancestors can still apply.
|