bireactive 0.2.4 → 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.
Files changed (96) hide show
  1. package/dist/animation/anim.js +4 -0
  2. package/dist/coll.d.ts +7 -7
  3. package/dist/core/cell.d.ts +89 -66
  4. package/dist/core/cell.js +642 -401
  5. package/dist/core/index.d.ts +4 -14
  6. package/dist/core/index.js +4 -14
  7. package/dist/core/lenses/aggregates.d.ts +1 -1
  8. package/dist/core/lenses/aggregates.js +4 -3
  9. package/dist/core/lenses/closed-form-policies.js +6 -6
  10. package/dist/core/lenses/decompositions.js +3 -3
  11. package/dist/core/lenses/domain-aggregates.js +5 -5
  12. package/dist/core/lenses/geometry.d.ts +1 -1
  13. package/dist/core/lenses/geometry.js +6 -7
  14. package/dist/core/lenses/memory.d.ts +2 -2
  15. package/dist/core/lenses/memory.js +3 -3
  16. package/dist/core/lenses/typed-factor.js +4 -3
  17. package/dist/core/traits.d.ts +1 -0
  18. package/dist/core/values/box.js +7 -7
  19. package/dist/core/values/color.js +5 -5
  20. package/dist/core/values/field.d.ts +70 -0
  21. package/dist/core/values/field.js +230 -0
  22. package/dist/core/values/gpu.d.ts +4 -2
  23. package/dist/core/values/gpu.js +11 -4
  24. package/dist/core/values/matrix.js +7 -7
  25. package/dist/core/values/num.d.ts +1 -1
  26. package/dist/core/values/num.js +1 -1
  27. package/dist/core/values/pose.js +4 -4
  28. package/dist/core/values/range.js +6 -6
  29. package/dist/core/values/template.d.ts +1 -1
  30. package/dist/core/values/template.js +2 -1
  31. package/dist/core/values/transform.js +7 -7
  32. package/dist/core/values/tri.js +3 -3
  33. package/dist/core/values/vec.js +8 -12
  34. package/dist/ext/timeline.js +2 -2
  35. package/dist/formats/cst.d.ts +127 -0
  36. package/dist/formats/cst.js +280 -0
  37. package/dist/formats/edn.d.ts +2 -0
  38. package/dist/formats/edn.js +301 -0
  39. package/dist/formats/index.d.ts +6 -0
  40. package/dist/formats/index.js +8 -0
  41. package/dist/formats/json.d.ts +2 -0
  42. package/dist/formats/json.js +332 -0
  43. package/dist/formats/lens.d.ts +8 -0
  44. package/dist/formats/lens.js +54 -0
  45. package/dist/formats/toml.d.ts +2 -0
  46. package/dist/formats/toml.js +526 -0
  47. package/dist/formats/yaml.d.ts +2 -0
  48. package/dist/formats/yaml.js +661 -0
  49. package/dist/index.d.ts +10 -0
  50. package/dist/index.js +10 -0
  51. package/dist/learn/data.d.ts +49 -0
  52. package/dist/learn/data.js +181 -0
  53. package/dist/learn/index.d.ts +3 -0
  54. package/dist/learn/index.js +6 -0
  55. package/dist/learn/lens-net.d.ts +63 -0
  56. package/dist/learn/lens-net.js +219 -0
  57. package/dist/learn/mlp.d.ts +77 -0
  58. package/dist/learn/mlp.js +292 -0
  59. package/dist/propagators/csp.d.ts +13 -0
  60. package/dist/propagators/csp.js +52 -0
  61. package/dist/propagators/flex.d.ts +31 -0
  62. package/dist/propagators/flex.js +189 -0
  63. package/dist/propagators/graph.d.ts +73 -0
  64. package/dist/propagators/graph.js +543 -0
  65. package/dist/propagators/index.d.ts +8 -6
  66. package/dist/propagators/index.js +15 -6
  67. package/dist/propagators/lattice.d.ts +45 -0
  68. package/dist/propagators/lattice.js +113 -0
  69. package/dist/propagators/layout.d.ts +1 -27
  70. package/dist/propagators/layout.js +6 -175
  71. package/dist/propagators/numeric.d.ts +17 -0
  72. package/dist/propagators/numeric.js +93 -0
  73. package/dist/propagators/solver.d.ts +51 -0
  74. package/dist/propagators/solver.js +175 -0
  75. package/dist/schema/index.d.ts +1 -0
  76. package/dist/schema/index.js +3 -0
  77. package/dist/schema/lens.d.ts +121 -0
  78. package/dist/schema/lens.js +429 -0
  79. package/dist/shapes/annular-sector.js +4 -4
  80. package/dist/shapes/button.js +1 -1
  81. package/dist/shapes/circle.js +1 -1
  82. package/dist/shapes/handle.js +2 -2
  83. package/dist/shapes/label.js +1 -1
  84. package/dist/shapes/layout.js +2 -2
  85. package/dist/shapes/rect.js +7 -7
  86. package/dist/shapes/shape.js +8 -8
  87. package/dist/web/diagram.js +2 -2
  88. package/package.json +1 -1
  89. package/dist/propagators/network.d.ts +0 -52
  90. package/dist/propagators/network.js +0 -185
  91. package/dist/propagators/propagator.d.ts +0 -12
  92. package/dist/propagators/propagator.js +0 -16
  93. package/dist/propagators/range.d.ts +0 -45
  94. package/dist/propagators/range.js +0 -147
  95. package/dist/propagators/relations.d.ts +0 -60
  96. package/dist/propagators/relations.js +0 -343
@@ -0,0 +1,121 @@
1
+ import { type Cell, type Writable } from "../core/cell.js";
2
+ /** A plain JSON-ish record. */
3
+ export type Obj = Record<string, unknown>;
4
+ /** A value-level bidirectional transform carrying complement `C`. */
5
+ export interface VLens<S, V, C> {
6
+ init: (s: S) => C;
7
+ /** Refresh the complement from a (possibly externally-changed) source. */
8
+ step?: (s: S, c: C) => C;
9
+ fwd: (s: S, c: C) => V;
10
+ bwd: (v: V, s: S, c: C) => {
11
+ s: S;
12
+ c: C;
13
+ };
14
+ }
15
+ /** An object→object lens (the common case). */
16
+ export type OLens = VLens<Obj, Obj, unknown>;
17
+ /** Rename a top-level field. Complement-honest about a colliding target:
18
+ * if the source already has `to`, that shadowed value is parked and
19
+ * restored on the way back (so GetPut holds even on malformed input). */
20
+ export declare function renameV(from: string, to: string): VLens<Obj, Obj, {
21
+ shadow: unknown;
22
+ }>;
23
+ /** A field a newer schema adds; the value lives in the complement. */
24
+ export declare function addV(key: string, initial: unknown): VLens<Obj, Obj, {
25
+ val: unknown;
26
+ }>;
27
+ /** A field a newer schema drops; the value (and position) live in the complement. */
28
+ export declare function removeV(key: string): VLens<Obj, Obj, {
29
+ val: unknown;
30
+ idx: number;
31
+ }>;
32
+ /** Move several top-level fields into a sub-object. Bijective. */
33
+ export declare function nestV(keys: readonly string[], under: string): VLens<Obj, Obj, null>;
34
+ /** How to split one string field into two and rejoin them. */
35
+ export interface SplitSpec {
36
+ split: (whole: string) => [string, string];
37
+ join: (a: string, b: string) => string;
38
+ }
39
+ /** Split one string field into two. Complement-honest: it stores the chosen
40
+ * halves AND the exact original string, so a clean read-write round-trip
41
+ * reproduces the source verbatim (trailing spaces, odd separators) instead
42
+ * of being re-guessed by `join(split(·))`. The lens Cambria couldn't make
43
+ * bidirectional ("firstName/lastName → fullName runs reliably one way"). */
44
+ export declare function splitV(key: string, into: readonly [string, string], spec: SplitSpec): VLens<Obj, Obj, {
45
+ a: string;
46
+ b: string;
47
+ whole: string;
48
+ }>;
49
+ /** A stateful 1→1 value transform on a single field's value. */
50
+ export interface FieldMap<C> {
51
+ init: (srcVal: unknown) => C;
52
+ step?: (srcVal: unknown, c: C) => C;
53
+ fwd: (srcVal: unknown, c: C) => unknown;
54
+ bwd: (viewVal: unknown, srcVal: unknown, c: C) => {
55
+ src: unknown;
56
+ complement: C;
57
+ };
58
+ }
59
+ /** Apply a value-lens to a single field, optionally renaming the key. The
60
+ * workhorse behind `mapField`, `wrapField`, and `into`. */
61
+ export declare function onField<S, V, C>(key: string, viewKey: string, vl: VLens<S, V, C>): VLens<Obj, Obj, C>;
62
+ /** Scalar → array. Forward wraps the scalar as the array head; the *tail*
63
+ * (everything the older scalar schema can't see) lives in the complement.
64
+ * Writing the scalar replaces only the head and CONSERVES the tail — the
65
+ * consistency Cambria's `head`/`wrap` pair couldn't keep (its Appendix III
66
+ * "defective implementation" clobbered or dropped the unseen elements). */
67
+ export declare function wrapV(): VLens<unknown, unknown[], {
68
+ tail: unknown[];
69
+ }>;
70
+ /** Array → scalar head (the dual of `wrapV`). The tail stays in the source
71
+ * array, so no complement is needed: the head view writes back as
72
+ * `[head, ...tail]`, conserving the rest. Reorder or grow the array and the
73
+ * scalar tracks the new head; edit the scalar and only the head moves. */
74
+ export declare function headV(): VLens<unknown[], unknown, null>;
75
+ /** Left-to-right composition of value-lenses. The complement type is the
76
+ * (heterogeneous) product of the parts', so it's existential here. */
77
+ export declare function seq(...lenses: VLens<Obj, Obj, any>[]): VLens<Obj, Obj, unknown>;
78
+ /** Descend into a field and apply a value-lens to whatever lives there
79
+ * (a nested object, or an array — see `each`). Cambria's `in`. */
80
+ export declare function into<S, V, C>(key: string, vl: VLens<S, V, C>): VLens<Obj, Obj, C>;
81
+ /** Apply an element lens to every item of an array, keyed by `by` for stable
82
+ * identity. Inserts (new key), deletes (missing key), and reorders (view
83
+ * order) all round-trip, and each element keeps its OWN complement across
84
+ * the move — the array case Cambria flagged as unbuilt (`mapInto` plus
85
+ * "merging or splitting arrays"). */
86
+ export declare function eachBy(by: (e: Obj) => unknown, elem: VLens<Obj, Obj, any>): VLens<Obj[], Obj[], Map<unknown, unknown>>;
87
+ /** Positional `each` (element i ↔ element i). Use `eachBy` when elements can
88
+ * reorder; this is fine for stable structures (e.g. recursive trees). */
89
+ export declare function each(elem: VLens<Obj, Obj, any>): VLens<Obj[], Obj[], unknown[]>;
90
+ /** Build a self-referential lens. `recurse(self => …)` lets a lens use itself
91
+ * for nested occurrences — e.g. rename a field at every level of a subtask
92
+ * tree of arbitrary depth. Cambria's open "recursive schemas" case. */
93
+ export declare function recurse(build: (self: OLens) => OLens): OLens;
94
+ /** Lift a value-lens onto a reactive cell. */
95
+ export declare function toStep<C>(vl: VLens<Obj, Obj, C>): Step;
96
+ /** A migration step: a writable lens from one POJO schema to the next. */
97
+ export type Step = (src: Writable<Cell<Obj>>) => Writable<Cell<Obj>>;
98
+ /** Left-to-right composition of reactive steps (chains cells). */
99
+ export declare function pipe(...steps: Step[]): Step;
100
+ /** Rename a top-level field. */
101
+ export declare function renameField(from: string, to: string): Step;
102
+ /** Add a field the source can't represent (value parked in the complement). */
103
+ export declare function addField(key: string, initial: unknown): Step;
104
+ /** Drop a field (value + position parked in the complement). */
105
+ export declare function removeField(key: string): Step;
106
+ /** Move several top-level fields into a sub-object. */
107
+ export declare function nestFields(keys: readonly string[], under: string): Step;
108
+ /** Split one string field into two (complement-honest; see {@link splitV}). */
109
+ export declare function splitField(key: string, into: readonly [string, string], spec: SplitSpec): Step;
110
+ /** Scalar → array on a field, optionally renaming it (see {@link wrapV}). */
111
+ export declare function wrapField(key: string, rename?: string): Step;
112
+ /** Array → scalar head on a field, optionally renaming it (see {@link headV}). */
113
+ export declare function headField(key: string, rename?: string): Step;
114
+ /** A stateful 1→1 value transform on a single field, optionally renaming it. */
115
+ export declare function mapField<C>(key: string, m: FieldMap<C> & {
116
+ rename?: string;
117
+ }): Step;
118
+ /** Apply a whole sub-migration inside a nested object field. Cambria's `in`. */
119
+ export declare function inField(key: string, vl: VLens<Obj, Obj, unknown>): Step;
120
+ /** Apply an element migration to every item of an array field, keyed by `by`. */
121
+ export declare function mapElems(key: string, by: (e: Obj) => unknown, elem: VLens<Obj, Obj, any>): Step;
@@ -0,0 +1,429 @@
1
+ // schema/lens.ts — composable, complement-carrying lenses between plain POJOs.
2
+ //
3
+ // Two layers:
4
+ //
5
+ // • VLens<S,V,C> — a *value-level* bidirectional transform carrying a
6
+ // complement `C` (what the forward direction drops). VLenses compose
7
+ // (`seq`), descend into a field (`into`), map over arrays element-wise
8
+ // (`each`/`eachBy`), and recurse (`recurse`). The complement of a
9
+ // composite is the product of its parts' complements, so a lossy
10
+ // pipeline still round-trips — the property Cambria lacked.
11
+ //
12
+ // • Step — a VLens lifted onto a reactive cell (`toStep`). `pipe` chains
13
+ // Steps into the A→B→{C,D} reactive graph used by the demo.
14
+ //
15
+ // Orientation: the source `S` is the older / upstream schema; `fwd` builds
16
+ // the newer view, `bwd` writes an edit back. Information a newer schema adds,
17
+ // or distinctions an older one can't hold, lives in `C` — local view state,
18
+ // never in the shared source.
19
+ import { lens } from "../core/cell.js";
20
+ /** Sentinel for "this key was absent" (distinct from an explicit `undefined`). */
21
+ const ABSENT = Symbol("absent");
22
+ // ── object helpers (order-preserving, pure) ─────────────────────────
23
+ function omit(v, key) {
24
+ const out = {};
25
+ for (const k of Object.keys(v))
26
+ if (k !== key)
27
+ out[k] = v[k];
28
+ return out;
29
+ }
30
+ function replaceKey(v, oldKey, newKey, newVal) {
31
+ const out = {};
32
+ let placed = false;
33
+ for (const k of Object.keys(v)) {
34
+ if (k === oldKey) {
35
+ out[newKey] = newVal;
36
+ placed = true;
37
+ }
38
+ else if (k === newKey && oldKey !== newKey) {
39
+ // drop a colliding target slot; the rename owns this key now
40
+ }
41
+ else {
42
+ out[k] = v[k];
43
+ }
44
+ }
45
+ if (!placed)
46
+ out[newKey] = newVal;
47
+ return out;
48
+ }
49
+ function keyIndex(v, key) {
50
+ return Object.keys(v).indexOf(key);
51
+ }
52
+ /** Reinsert `key` (absent from `v`) at position `idx`. */
53
+ function insertAt(v, key, val, idx) {
54
+ const keys = Object.keys(v).filter(k => k !== key);
55
+ const at = idx < 0 ? keys.length : Math.min(idx, keys.length);
56
+ keys.splice(at, 0, key);
57
+ const out = {};
58
+ for (const k of keys)
59
+ out[k] = k === key ? val : v[k];
60
+ return out;
61
+ }
62
+ function insertPair(v, key, ka, va, kb, vb) {
63
+ const out = {};
64
+ for (const k of Object.keys(v)) {
65
+ if (k === key) {
66
+ out[ka] = va;
67
+ out[kb] = vb;
68
+ }
69
+ else {
70
+ out[k] = v[k];
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ function collapsePair(v, ka, kb, key, whole) {
76
+ const out = {};
77
+ let placed = false;
78
+ for (const k of Object.keys(v)) {
79
+ if (k === ka || k === kb) {
80
+ if (!placed) {
81
+ out[key] = whole;
82
+ placed = true;
83
+ }
84
+ }
85
+ else {
86
+ out[k] = v[k];
87
+ }
88
+ }
89
+ if (!placed)
90
+ out[key] = whole;
91
+ return out;
92
+ }
93
+ function nestObj(v, keys, under) {
94
+ const set = new Set(keys);
95
+ const sub = {};
96
+ for (const k of keys)
97
+ if (k in v)
98
+ sub[k] = v[k];
99
+ const out = {};
100
+ let placed = false;
101
+ for (const k of Object.keys(v)) {
102
+ if (set.has(k)) {
103
+ if (!placed) {
104
+ out[under] = sub;
105
+ placed = true;
106
+ }
107
+ }
108
+ else {
109
+ out[k] = v[k];
110
+ }
111
+ }
112
+ if (!placed)
113
+ out[under] = sub;
114
+ return out;
115
+ }
116
+ function unnestObj(v, keys, under) {
117
+ const sub = v[under] ?? {};
118
+ const out = {};
119
+ for (const k of Object.keys(v)) {
120
+ if (k === under) {
121
+ for (const kk of keys)
122
+ out[kk] = sub[kk];
123
+ }
124
+ else {
125
+ out[k] = v[k];
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ // ── value-level primitives ──────────────────────────────────────────
131
+ /** Rename a top-level field. Complement-honest about a colliding target:
132
+ * if the source already has `to`, that shadowed value is parked and
133
+ * restored on the way back (so GetPut holds even on malformed input). */
134
+ export function renameV(from, to) {
135
+ return {
136
+ init: v => ({ shadow: to in v ? v[to] : ABSENT }),
137
+ step: v => ({ shadow: to in v ? v[to] : ABSENT }),
138
+ fwd: v => replaceKey(v, from, to, v[from]),
139
+ bwd: (t, _v, c) => {
140
+ let s = replaceKey(t, to, from, t[to]);
141
+ if (c.shadow !== ABSENT)
142
+ s = { ...s, [to]: c.shadow };
143
+ return { s, c };
144
+ },
145
+ };
146
+ }
147
+ /** A field a newer schema adds; the value lives in the complement. */
148
+ export function addV(key, initial) {
149
+ return {
150
+ init: () => ({ val: initial }),
151
+ fwd: (v, c) => ({ ...v, [key]: c.val }),
152
+ bwd: t => ({ s: omit(t, key), c: { val: t[key] } }),
153
+ };
154
+ }
155
+ /** A field a newer schema drops; the value (and position) live in the complement. */
156
+ export function removeV(key) {
157
+ const capture = (v) => ({ val: v[key], idx: keyIndex(v, key) });
158
+ return {
159
+ init: capture,
160
+ step: (v, c) => (key in v ? capture(v) : c),
161
+ fwd: v => omit(v, key),
162
+ bwd: (t, _v, c) => ({ s: insertAt(t, key, c.val, c.idx), c }),
163
+ };
164
+ }
165
+ /** Move several top-level fields into a sub-object. Bijective. */
166
+ export function nestV(keys, under) {
167
+ return {
168
+ init: () => null,
169
+ fwd: v => nestObj(v, keys, under),
170
+ bwd: t => ({ s: unnestObj(t, keys, under), c: null }),
171
+ };
172
+ }
173
+ /** Split one string field into two. Complement-honest: it stores the chosen
174
+ * halves AND the exact original string, so a clean read-write round-trip
175
+ * reproduces the source verbatim (trailing spaces, odd separators) instead
176
+ * of being re-guessed by `join(split(·))`. The lens Cambria couldn't make
177
+ * bidirectional ("firstName/lastName → fullName runs reliably one way"). */
178
+ export function splitV(key, into, spec) {
179
+ const [ka, kb] = into;
180
+ const part = (whole) => {
181
+ const [a, b] = spec.split(whole);
182
+ return { a, b, whole };
183
+ };
184
+ return {
185
+ init: v => part(String(v[key] ?? "")),
186
+ step: (v, c) => {
187
+ const whole = String(v[key] ?? "");
188
+ return whole === c.whole ? c : part(whole);
189
+ },
190
+ fwd: (v, c) => insertPair(v, key, ka, c.a, kb, c.b),
191
+ bwd: (t, _v, c) => {
192
+ const a = String(t[ka] ?? "");
193
+ const b = String(t[kb] ?? "");
194
+ // Parts unchanged ⇒ restore the original whole exactly (GetPut).
195
+ const whole = a === c.a && b === c.b ? c.whole : spec.join(a, b);
196
+ return { s: collapsePair(t, ka, kb, key, whole), c: { a, b, whole } };
197
+ },
198
+ };
199
+ }
200
+ /** Apply a value-lens to a single field, optionally renaming the key. The
201
+ * workhorse behind `mapField`, `wrapField`, and `into`. */
202
+ export function onField(key, viewKey, vl) {
203
+ return {
204
+ init: v => vl.init(v[key]),
205
+ step: (v, c) => (vl.step ? vl.step(v[key], c) : c),
206
+ fwd: (v, c) => replaceKey(v, key, viewKey, vl.fwd(v[key], c)),
207
+ bwd: (t, v, c) => {
208
+ const r = vl.bwd(t[viewKey], v[key], c);
209
+ return { s: replaceKey(t, viewKey, key, r.s), c: r.c };
210
+ },
211
+ };
212
+ }
213
+ function fieldMapToVL(m) {
214
+ return {
215
+ init: m.init,
216
+ step: m.step,
217
+ fwd: m.fwd,
218
+ bwd: (v, s, c) => {
219
+ const r = m.bwd(v, s, c);
220
+ return { s: r.src, c: r.complement };
221
+ },
222
+ };
223
+ }
224
+ /** Scalar → array. Forward wraps the scalar as the array head; the *tail*
225
+ * (everything the older scalar schema can't see) lives in the complement.
226
+ * Writing the scalar replaces only the head and CONSERVES the tail — the
227
+ * consistency Cambria's `head`/`wrap` pair couldn't keep (its Appendix III
228
+ * "defective implementation" clobbered or dropped the unseen elements). */
229
+ export function wrapV() {
230
+ return {
231
+ init: () => ({ tail: [] }),
232
+ fwd: (s, c) => [s, ...c.tail],
233
+ bwd: arr => {
234
+ const a = Array.isArray(arr) ? arr : [arr];
235
+ return { s: a.length ? a[0] : null, c: { tail: a.slice(1) } };
236
+ },
237
+ };
238
+ }
239
+ /** Array → scalar head (the dual of `wrapV`). The tail stays in the source
240
+ * array, so no complement is needed: the head view writes back as
241
+ * `[head, ...tail]`, conserving the rest. Reorder or grow the array and the
242
+ * scalar tracks the new head; edit the scalar and only the head moves. */
243
+ export function headV() {
244
+ return {
245
+ init: () => null,
246
+ fwd: arr => (Array.isArray(arr) && arr.length ? arr[0] : null),
247
+ bwd: (head, arr) => ({ s: [head, ...(Array.isArray(arr) ? arr.slice(1) : [])], c: null }),
248
+ };
249
+ }
250
+ // ── combinators ──────────────────────────────────────────────────────
251
+ /** Compose two value-lenses (the product complement is `[c1, c2]`). */
252
+ function then2(l1, l2) {
253
+ return {
254
+ init: a => {
255
+ const c1 = l1.init(a);
256
+ return [c1, l2.init(l1.fwd(a, c1))];
257
+ },
258
+ step: (a, [c1, c2]) => {
259
+ const c1b = l1.step ? l1.step(a, c1) : c1;
260
+ const b = l1.fwd(a, c1b);
261
+ const c2b = l2.step ? l2.step(b, c2) : c2;
262
+ return [c1b, c2b];
263
+ },
264
+ fwd: (a, [c1, c2]) => l2.fwd(l1.fwd(a, c1), c2),
265
+ bwd: (d, a, [c1, c2]) => {
266
+ const b = l1.fwd(a, c1);
267
+ const r2 = l2.bwd(d, b, c2);
268
+ const r1 = l1.bwd(r2.s, a, c1);
269
+ return { s: r1.s, c: [r1.c, r2.c] };
270
+ },
271
+ };
272
+ }
273
+ /** Left-to-right composition of value-lenses. The complement type is the
274
+ * (heterogeneous) product of the parts', so it's existential here. */
275
+ // biome-ignore lint/suspicious/noExplicitAny: complements compose existentially
276
+ export function seq(...lenses) {
277
+ if (lenses.length === 0) {
278
+ return { init: () => null, fwd: v => v, bwd: t => ({ s: t, c: null }) };
279
+ }
280
+ return lenses.reduce((acc, l) => then2(acc, l));
281
+ }
282
+ /** Descend into a field and apply a value-lens to whatever lives there
283
+ * (a nested object, or an array — see `each`). Cambria's `in`. */
284
+ export function into(key, vl) {
285
+ return onField(key, key, vl);
286
+ }
287
+ /** Apply an element lens to every item of an array, keyed by `by` for stable
288
+ * identity. Inserts (new key), deletes (missing key), and reorders (view
289
+ * order) all round-trip, and each element keeps its OWN complement across
290
+ * the move — the array case Cambria flagged as unbuilt (`mapInto` plus
291
+ * "merging or splitting arrays"). */
292
+ export function eachBy(by,
293
+ // biome-ignore lint/suspicious/noExplicitAny: per-element complement is existential
294
+ elem) {
295
+ return {
296
+ init: arr => {
297
+ const m = new Map();
298
+ for (const el of arr)
299
+ m.set(by(el), elem.init(el));
300
+ return m;
301
+ },
302
+ step: (arr, c) => {
303
+ const m = new Map();
304
+ for (const el of arr) {
305
+ const k = by(el);
306
+ const ci = c.has(k) ? c.get(k) : elem.init(el);
307
+ m.set(k, elem.step ? elem.step(el, ci) : ci);
308
+ }
309
+ return m;
310
+ },
311
+ fwd: (arr, c) => arr.map(el => elem.fwd(el, c.has(by(el)) ? c.get(by(el)) : elem.init(el))),
312
+ bwd: (view, arr, c) => {
313
+ const srcByKey = new Map();
314
+ for (const el of arr)
315
+ srcByKey.set(by(el), el);
316
+ const nc = new Map();
317
+ const s = view.map(vel => {
318
+ const k = by(vel);
319
+ const s0 = srcByKey.get(k) ?? {};
320
+ const ci = c.has(k) ? c.get(k) : elem.init(s0);
321
+ const r = elem.bwd(vel, s0, ci);
322
+ nc.set(k, r.c);
323
+ return r.s;
324
+ });
325
+ return { s, c: nc };
326
+ },
327
+ };
328
+ }
329
+ /** Positional `each` (element i ↔ element i). Use `eachBy` when elements can
330
+ * reorder; this is fine for stable structures (e.g. recursive trees). */
331
+ // biome-ignore lint/suspicious/noExplicitAny: per-element complement is existential
332
+ export function each(elem) {
333
+ return {
334
+ init: arr => arr.map(el => elem.init(el)),
335
+ step: (arr, c) => arr.map((el, i) => {
336
+ const ci = i < c.length ? c[i] : elem.init(el);
337
+ return elem.step ? elem.step(el, ci) : ci;
338
+ }),
339
+ fwd: (arr, c) => arr.map((el, i) => elem.fwd(el, i < c.length ? c[i] : elem.init(el))),
340
+ bwd: (view, arr, c) => {
341
+ const nc = [];
342
+ const s = view.map((vel, i) => {
343
+ const s0 = arr[i] ?? {};
344
+ const ci = i < c.length ? c[i] : elem.init(s0);
345
+ const r = elem.bwd(vel, s0, ci);
346
+ nc.push(r.c);
347
+ return r.s;
348
+ });
349
+ return { s, c: nc };
350
+ },
351
+ };
352
+ }
353
+ /** Build a self-referential lens. `recurse(self => …)` lets a lens use itself
354
+ * for nested occurrences — e.g. rename a field at every level of a subtask
355
+ * tree of arbitrary depth. Cambria's open "recursive schemas" case. */
356
+ export function recurse(build) {
357
+ // Lazy thunk: the body references `self`, whose methods defer to the built
358
+ // lens. Construction terminates; recursion only unfolds against finite data.
359
+ let built = null;
360
+ const self = {
361
+ init: s => (built ??= build(self)).init(s),
362
+ step: (s, c) => {
363
+ const b = (built ??= build(self));
364
+ return b.step ? b.step(s, c) : c;
365
+ },
366
+ fwd: (s, c) => (built ??= build(self)).fwd(s, c),
367
+ bwd: (v, s, c) => (built ??= build(self)).bwd(v, s, c),
368
+ };
369
+ return self;
370
+ }
371
+ // ── reactive lifting (cells) ─────────────────────────────────────────
372
+ /** Lift a value-lens onto a reactive cell. */
373
+ export function toStep(vl) {
374
+ return src => lens(src, {
375
+ init: ([v]) => vl.init(v),
376
+ step: ([v], c) => (vl.step ? vl.step(v, c) : c),
377
+ fwd: ([v], c) => vl.fwd(v, c),
378
+ bwd: (t, [v], c) => {
379
+ const r = vl.bwd(t, v, c);
380
+ return { updates: [r.s], complement: r.c };
381
+ },
382
+ });
383
+ }
384
+ /** Left-to-right composition of reactive steps (chains cells). */
385
+ export function pipe(...steps) {
386
+ return src => steps.reduce((acc, step) => step(acc), src);
387
+ }
388
+ // ── public Step-returning kit (sugar over the value-level core) ──────
389
+ /** Rename a top-level field. */
390
+ export function renameField(from, to) {
391
+ return toStep(renameV(from, to));
392
+ }
393
+ /** Add a field the source can't represent (value parked in the complement). */
394
+ export function addField(key, initial) {
395
+ return toStep(addV(key, initial));
396
+ }
397
+ /** Drop a field (value + position parked in the complement). */
398
+ export function removeField(key) {
399
+ return toStep(removeV(key));
400
+ }
401
+ /** Move several top-level fields into a sub-object. */
402
+ export function nestFields(keys, under) {
403
+ return toStep(nestV(keys, under));
404
+ }
405
+ /** Split one string field into two (complement-honest; see {@link splitV}). */
406
+ export function splitField(key, into, spec) {
407
+ return toStep(splitV(key, into, spec));
408
+ }
409
+ /** Scalar → array on a field, optionally renaming it (see {@link wrapV}). */
410
+ export function wrapField(key, rename) {
411
+ return toStep(onField(key, rename ?? key, wrapV()));
412
+ }
413
+ /** Array → scalar head on a field, optionally renaming it (see {@link headV}). */
414
+ export function headField(key, rename) {
415
+ return toStep(onField(key, rename ?? key, headV()));
416
+ }
417
+ /** A stateful 1→1 value transform on a single field, optionally renaming it. */
418
+ export function mapField(key, m) {
419
+ return toStep(onField(key, m.rename ?? key, fieldMapToVL(m)));
420
+ }
421
+ /** Apply a whole sub-migration inside a nested object field. Cambria's `in`. */
422
+ export function inField(key, vl) {
423
+ return toStep(into(key, vl));
424
+ }
425
+ /** Apply an element migration to every item of an array field, keyed by `by`. */
426
+ // biome-ignore lint/suspicious/noExplicitAny: per-element complement is existential
427
+ export function mapElems(key, by, elem) {
428
+ return toStep(into(key, eachBy(by, elem)));
429
+ }
@@ -7,10 +7,10 @@ export class AnnularSector extends Shape {
7
7
  a0;
8
8
  a1;
9
9
  constructor(center, rOuter, rInner, a0, a1, opts = {}) {
10
- const ro = Num.from(rOuter);
11
- const ri = Num.from(rInner);
12
- const a0s = Num.from(a0);
13
- const a1s = Num.from(a1);
10
+ const ro = Num.coerce(rOuter);
11
+ const ri = Num.coerce(rInner);
12
+ const a0s = Num.coerce(a0);
13
+ const a1s = Num.coerce(a1);
14
14
  super("path", () => ({
15
15
  x: center.x.value - ro.value,
16
16
  y: center.y.value - ro.value,
@@ -11,7 +11,7 @@ import { tokens } from "./tokens.js";
11
11
  export function button(pos, content, onClick, opts = {}) {
12
12
  const w = opts.width ?? 80;
13
13
  const h = opts.height ?? 26;
14
- const size = Num.from(opts.size ?? 11);
14
+ const size = Num.coerce(opts.size ?? 11);
15
15
  const hovered = opts.hovered ?? cell(false);
16
16
  // Hover tint behind the border so outline weight stays constant.
17
17
  const g = group({ translate: pos }, rect(0, 0, w, h, {
@@ -4,7 +4,7 @@ import { Shape } from "./shape.js";
4
4
  export class Circle extends Shape {
5
5
  radius;
6
6
  constructor(center, radius, opts = {}) {
7
- const r = Num.from(radius);
7
+ const r = Num.coerce(radius);
8
8
  super(opts.dashed ? "path" : "circle", () => ({
9
9
  x: center.x.value - r.value,
10
10
  y: center.y.value - r.value,
@@ -1,5 +1,5 @@
1
1
  // handle.* — writable derived shapes (draggable circles wired to a Vec).
2
- import { cell, mean, polar as polarLens, Vec, } from "../core/index.js";
2
+ import { cell, mean, polar as polarLens, SKIP, Vec, } from "../core/index.js";
3
3
  import { Circle } from "./circle.js";
4
4
  import { drag } from "./interaction.js";
5
5
  const COLOR = "var(--bireactive-handle, #2563eb)";
@@ -51,7 +51,7 @@ const scaleHandle = (shape, radius = 40, opts) => {
51
51
  // Reads center and scale; writes only scale.
52
52
  const pos = Vec.lens([shape.center, shape.scale], vals => ({ x: vals[0].x + radius * vals[1].x, y: vals[0].y }), (target, vals) => {
53
53
  const k = Math.max(0.05, Math.abs(target.x - vals[0].x) / radius);
54
- return [undefined, { x: k, y: k }];
54
+ return [SKIP, { x: k, y: k }];
55
55
  });
56
56
  return handleFn(pos, { cursor: "ew-resize", ...opts });
57
57
  };
@@ -14,7 +14,7 @@ export class Label extends Shape {
14
14
  : typeof content === "function"
15
15
  ? derive(content)
16
16
  : cell(content);
17
- const sizeSig = Num.from(opts.size ?? tokens.fontSize);
17
+ const sizeSig = Num.coerce(opts.size ?? tokens.fontSize);
18
18
  const a = opts.align ?? { x: 0.5, y: 0.5 };
19
19
  super("text", () => {
20
20
  const text = flattenText(contentSig.value);
@@ -1,5 +1,5 @@
1
1
  // Spatial composition primitives.
2
- import { Box, BoxMath, reader, transformBox } from "../core/index.js";
2
+ import { Box, boxExpand, reader, transformBox } from "../core/index.js";
3
3
  /** Lay out `shapes` in a row/column. First stays put; the rest bind
4
4
  * their `translate` reactively to sit `gap` past the previous.
5
5
  * Reflows on size or anchor change. */
@@ -40,7 +40,7 @@ export function arrange(shapes, axis, opts = {}) {
40
40
  /** Inflate a Box on each side by `by`. */
41
41
  export function expand(b, by) {
42
42
  const byFn = reader(by);
43
- return Box.derive(() => BoxMath.expand(b.value, byFn()));
43
+ return Box.derive(() => boxExpand(b.value, byFn()));
44
44
  }
45
45
  /** Split a Box along an axis into N reactive sub-Boxes.
46
46
  *
@@ -9,10 +9,10 @@ export class Rect extends Shape {
9
9
  h;
10
10
  corner;
11
11
  constructor(x, y, w, h, opts = {}) {
12
- const xs = Num.from(x);
13
- const ys = Num.from(y);
14
- const ws = Num.from(w);
15
- const hs = Num.from(h);
12
+ const xs = Num.coerce(x);
13
+ const ys = Num.coerce(y);
14
+ const ws = Num.coerce(w);
15
+ const hs = Num.coerce(h);
16
16
  super(opts.dashed ? "path" : "rect", () => ({ x: xs.value, y: ys.value, w: ws.value, h: hs.value }), opts, {
17
17
  origin: derive(() => ({
18
18
  x: xs.value + ws.value / 2,
@@ -23,7 +23,7 @@ export class Rect extends Shape {
23
23
  this.y = ys;
24
24
  this.w = ws;
25
25
  this.h = hs;
26
- this.corner = Num.from(opts.corner ?? tokens.corner);
26
+ this.corner = Num.coerce(opts.corner ?? tokens.corner);
27
27
  this.stroke(opts, true, {
28
28
  x: xs,
29
29
  y: ys,
@@ -123,8 +123,8 @@ export function rect(a, b, c, d, e) {
123
123
  if (a instanceof Vec) {
124
124
  const w = b;
125
125
  const h = c;
126
- const ws = Num.from(w);
127
- const hs = Num.from(h);
126
+ const ws = Num.coerce(w);
127
+ const hs = Num.coerce(h);
128
128
  return new Rect(derive(() => a.x.value - ws.value / 2), derive(() => a.y.value - hs.value / 2), ws, hs, d);
129
129
  }
130
130
  return new Rect(a, b, c, d, e);