bireactive 0.2.3 → 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 (98) hide show
  1. package/dist/animation/anim.js +4 -0
  2. package/dist/coll.d.ts +7 -7
  3. package/dist/coll.js +3 -1
  4. package/dist/core/cell.d.ts +89 -66
  5. package/dist/core/cell.js +642 -401
  6. package/dist/core/index.d.ts +4 -14
  7. package/dist/core/index.js +4 -14
  8. package/dist/core/lenses/aggregates.d.ts +1 -1
  9. package/dist/core/lenses/aggregates.js +4 -3
  10. package/dist/core/lenses/closed-form-policies.js +6 -6
  11. package/dist/core/lenses/decompositions.js +3 -3
  12. package/dist/core/lenses/domain-aggregates.js +5 -5
  13. package/dist/core/lenses/geometry.d.ts +1 -1
  14. package/dist/core/lenses/geometry.js +6 -7
  15. package/dist/core/lenses/memory.d.ts +2 -2
  16. package/dist/core/lenses/memory.js +3 -3
  17. package/dist/core/lenses/typed-factor.js +4 -3
  18. package/dist/core/traits.d.ts +1 -0
  19. package/dist/core/values/box.js +7 -7
  20. package/dist/core/values/color.js +5 -5
  21. package/dist/core/values/field.d.ts +70 -0
  22. package/dist/core/values/field.js +230 -0
  23. package/dist/core/values/gpu.d.ts +4 -2
  24. package/dist/core/values/gpu.js +11 -4
  25. package/dist/core/values/matrix.js +7 -7
  26. package/dist/core/values/num.d.ts +1 -1
  27. package/dist/core/values/num.js +1 -1
  28. package/dist/core/values/pose.js +4 -4
  29. package/dist/core/values/range.js +6 -6
  30. package/dist/core/values/template.d.ts +1 -1
  31. package/dist/core/values/template.js +2 -1
  32. package/dist/core/values/transform.js +7 -7
  33. package/dist/core/values/tri.js +3 -3
  34. package/dist/core/values/vec.js +8 -12
  35. package/dist/ext/timeline.js +2 -2
  36. package/dist/formats/cst.d.ts +127 -0
  37. package/dist/formats/cst.js +280 -0
  38. package/dist/formats/edn.d.ts +2 -0
  39. package/dist/formats/edn.js +301 -0
  40. package/dist/formats/index.d.ts +6 -0
  41. package/dist/formats/index.js +8 -0
  42. package/dist/formats/json.d.ts +2 -0
  43. package/dist/formats/json.js +332 -0
  44. package/dist/formats/lens.d.ts +8 -0
  45. package/dist/formats/lens.js +54 -0
  46. package/dist/formats/toml.d.ts +2 -0
  47. package/dist/formats/toml.js +526 -0
  48. package/dist/formats/yaml.d.ts +2 -0
  49. package/dist/formats/yaml.js +661 -0
  50. package/dist/index.d.ts +10 -0
  51. package/dist/index.js +10 -0
  52. package/dist/learn/data.d.ts +49 -0
  53. package/dist/learn/data.js +181 -0
  54. package/dist/learn/index.d.ts +3 -0
  55. package/dist/learn/index.js +6 -0
  56. package/dist/learn/lens-net.d.ts +63 -0
  57. package/dist/learn/lens-net.js +219 -0
  58. package/dist/learn/mlp.d.ts +77 -0
  59. package/dist/learn/mlp.js +292 -0
  60. package/dist/propagators/csp.d.ts +13 -0
  61. package/dist/propagators/csp.js +52 -0
  62. package/dist/propagators/flex.d.ts +31 -0
  63. package/dist/propagators/flex.js +189 -0
  64. package/dist/propagators/graph.d.ts +73 -0
  65. package/dist/propagators/graph.js +543 -0
  66. package/dist/propagators/index.d.ts +8 -6
  67. package/dist/propagators/index.js +15 -6
  68. package/dist/propagators/lattice.d.ts +45 -0
  69. package/dist/propagators/lattice.js +113 -0
  70. package/dist/propagators/layout.d.ts +1 -27
  71. package/dist/propagators/layout.js +6 -175
  72. package/dist/propagators/numeric.d.ts +17 -0
  73. package/dist/propagators/numeric.js +93 -0
  74. package/dist/propagators/solver.d.ts +51 -0
  75. package/dist/propagators/solver.js +175 -0
  76. package/dist/schema/index.d.ts +1 -0
  77. package/dist/schema/index.js +3 -0
  78. package/dist/schema/lens.d.ts +121 -0
  79. package/dist/schema/lens.js +429 -0
  80. package/dist/shapes/annular-sector.js +4 -4
  81. package/dist/shapes/button.js +1 -1
  82. package/dist/shapes/circle.js +1 -1
  83. package/dist/shapes/handle.js +2 -2
  84. package/dist/shapes/label.js +1 -1
  85. package/dist/shapes/layout.js +2 -2
  86. package/dist/shapes/rect.js +7 -7
  87. package/dist/shapes/shape.js +8 -8
  88. package/dist/tex/tex.js +9 -2
  89. package/dist/web/diagram.js +2 -2
  90. package/package.json +9 -19
  91. package/dist/propagators/network.d.ts +0 -52
  92. package/dist/propagators/network.js +0 -185
  93. package/dist/propagators/propagator.d.ts +0 -12
  94. package/dist/propagators/propagator.js +0 -16
  95. package/dist/propagators/range.d.ts +0 -45
  96. package/dist/propagators/range.js +0 -147
  97. package/dist/propagators/relations.d.ts +0 -60
  98. package/dist/propagators/relations.js +0 -343
@@ -0,0 +1,113 @@
1
+ // lattice.ts — the partial-information substrate for real propagators.
2
+ //
3
+ // A propagator network is monotone: cells hold *partial knowledge* that
4
+ // only ever sharpens. The thing that makes that work is a lattice —
5
+ // a `top` (no information), a `meet` (combine two pieces of knowledge),
6
+ // and a `bottom` test (the knowledge contradicts itself).
7
+ //
8
+ // Because `meet` only narrows and lattices here have well-founded
9
+ // descent (finite sets shrink; intervals shrink toward a point),
10
+ // fixpoint iteration TERMINATES by construction. No fuel cap, no
11
+ // divergence panic — the structure of the lattice is the guarantee.
12
+ //
13
+ // Two instances cover everything downstream:
14
+ // • `interval` — `[lo, hi]` numeric bounds (layout, ranges, layering)
15
+ // • `set(universe)` — finite candidate sets (CSP, sudoku, type inference)
16
+ //
17
+ // A `LatticeCell<T>` is a plain `cell<T>` tagged with its lattice (via a
18
+ // WeakMap), so `merge()` and the solver can narrow generically without a
19
+ // new cell type — the non-coloring property is preserved.
20
+ import { cell } from "../core/index.js";
21
+ const latticeOf = new WeakMap();
22
+ /** The lattice a cell was minted with, or `undefined` for a plain cell. */
23
+ export function latticeFor(c) {
24
+ return latticeOf.get(c);
25
+ }
26
+ /** Mint a cell over `lat`, seeded at `top` (no information) by default. */
27
+ export function latticeCell(lat, init = lat.top) {
28
+ const c = cell(init, { equals: lat.equals });
29
+ latticeOf.set(c, lat);
30
+ return c;
31
+ }
32
+ /** Narrow `c` by `info` (monotone meet). No-ops when nothing sharpens, so
33
+ * it's safe to call in any order and any number of times. Returns true
34
+ * iff the cell actually narrowed. */
35
+ export function merge(c, info) {
36
+ const lat = latticeOf.get(c);
37
+ if (lat === undefined)
38
+ throw new Error("merge: cell was not minted via latticeCell");
39
+ const cur = c.peek();
40
+ const next = lat.meet(cur, info);
41
+ if (lat.equals(next, cur))
42
+ return false;
43
+ c.value = next;
44
+ return true;
45
+ }
46
+ /** True when the cell's knowledge has collapsed to a contradiction. */
47
+ export function isContradiction(c) {
48
+ const lat = latticeOf.get(c);
49
+ return lat?.isBottom(c.peek()) ?? false;
50
+ }
51
+ /** True when the cell still holds no information (its lattice `top`). */
52
+ export function isTop(c) {
53
+ const lat = latticeOf.get(c);
54
+ if (lat === undefined)
55
+ return false;
56
+ return lat.equals(c.peek(), lat.top);
57
+ }
58
+ /** Float slop so a hair of rounding doesn't read as a contradiction. */
59
+ const EPS = 1e-9;
60
+ export const interval = {
61
+ top: [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY],
62
+ meet: (a, b) => [Math.max(a[0], b[0]), Math.min(a[1], b[1])],
63
+ equals: (a, b) => a[0] === b[0] && a[1] === b[1],
64
+ isBottom: a => a[0] > a[1] + EPS,
65
+ };
66
+ /** Interval cell, optionally seeded with bounds (defaults to `top`). */
67
+ export function intervalCell(lo = Number.NEGATIVE_INFINITY, hi = Number.POSITIVE_INFINITY) {
68
+ return latticeCell(interval, [lo, hi]);
69
+ }
70
+ /** Width of an interval (`Infinity` if unbounded, negative if bottom). */
71
+ export function width(i) {
72
+ return i[1] - i[0];
73
+ }
74
+ /** A single value, or `undefined` when the interval isn't a point. */
75
+ export function point(i) {
76
+ return i[1] - i[0] <= EPS && Number.isFinite(i[0]) ? (i[0] + i[1]) / 2 : undefined;
77
+ }
78
+ // ── set lattice ─────────────────────────────────────────────────────
79
+ /** A finite candidate-set lattice over `universe`. `top` is the whole
80
+ * universe (all candidates open); `meet` intersects; bottom is empty.
81
+ * Height is `|universe|`, so narrowing always terminates. */
82
+ export function set(universe) {
83
+ const top = new Set(universe);
84
+ const eq = (a, b) => {
85
+ if (a === b)
86
+ return true;
87
+ if (a.size !== b.size)
88
+ return false;
89
+ for (const v of a)
90
+ if (!b.has(v))
91
+ return false;
92
+ return true;
93
+ };
94
+ return {
95
+ top,
96
+ meet: (a, b) => {
97
+ const [small, big] = a.size <= b.size ? [a, b] : [b, a];
98
+ const out = new Set();
99
+ for (const v of small)
100
+ if (big.has(v))
101
+ out.add(v);
102
+ return out;
103
+ },
104
+ equals: eq,
105
+ isBottom: a => a.size === 0,
106
+ };
107
+ }
108
+ /** Candidate-set cell over `universe`, seeded with `init` (defaults to
109
+ * the whole universe). */
110
+ export function setCell(universe, init) {
111
+ const lat = set(universe);
112
+ return latticeCell(lat, init === undefined ? lat.top : new Set(init));
113
+ }
@@ -1,32 +1,6 @@
1
1
  import { type Box, type Read } from "../core/index.js";
2
- import { type Propagator } from "./propagator.js";
2
+ import { type Propagator } from "./solver.js";
3
3
  type ValOrSig = number | Read<number>;
4
- /** Item in an `hstack` / `vstack`. Bare `Box` uses defaults (grow 1,
5
- * shrink 1, no min/max); tag for per-item flex. Per-item opts are
6
- * plain numbers (not reactive). */
7
- export type StackItem = Box | {
8
- box: Box;
9
- grow?: number;
10
- shrink?: number;
11
- min?: number;
12
- max?: number;
13
- };
14
- export interface StackOpts {
15
- /** Space between adjacent items. Default 0. */
16
- gap?: ValOrSig;
17
- /** Padding inside container on each side. Default 0. */
18
- padding?: ValOrSig;
19
- /** Cross-axis alignment. Default "start". */
20
- align?: "start" | "center" | "end" | "stretch";
21
- /** "fit" (default): items grow/shrink to fill container.
22
- * "hug": items keep their authored size; container resizes. */
23
- mode?: "fit" | "hug";
24
- }
25
- /** Horizontal CSS-flex stack: per-item grow/shrink/min/max along the
26
- * main axis, cross-axis handled by `align`. */
27
- export declare function hstack(c: Box, items: readonly StackItem[], opts?: StackOpts): Propagator;
28
- /** Vertical stack — top-to-bottom version of `hstack`. */
29
- export declare function vstack(c: Box, items: readonly StackItem[], opts?: StackOpts): Propagator;
30
4
  export interface GridOpts {
31
5
  /** Cells per row. */
32
6
  cols: number;
@@ -1,184 +1,15 @@
1
- // layout.ts — Box-relational layout combinators.
1
+ // layout.ts — rigid Box-relational combinators.
2
2
  //
3
- // Every combinator operates on `Box` value-types. Reactive opts
4
- // (`gap`, `padding`, ) accept a number or a Num signal.
5
- //
6
- // const c = box(0, 0, 300, 200);
7
- // const items = [box(), box(), box()];
8
- // p.add(hstack(c, items, { gap: 8, align: "stretch" }));
9
- //
10
- // `hstack` / `vstack` are CSS-flex-shaped (per-item grow/shrink vs
11
- // min/max). For rigid edge-to-edge layouts use `attach`,
12
- // `centerInside`, etc.
3
+ // The bidirectional, whole-box anchoring relations (edge-to-edge,
4
+ // centring, insets, grids). Flex lives in `flex.ts`; these are the
5
+ // fixed structural pieces you compose around it. Reactive opts (`gap`,
6
+ // `padding`, …) accept a number or a Num signal.
13
7
  import { isCell, readNow, } from "../core/index.js";
14
- import { propagator } from "./propagator.js";
8
+ import { propagator } from "./solver.js";
15
9
  const asW = (n) => n;
16
10
  function readDeps(...vs) {
17
11
  return vs.filter(isCell);
18
12
  }
19
- function clamp(v, lo, hi) {
20
- return Math.min(Math.max(v, lo), hi);
21
- }
22
- function specs(items) {
23
- return items.map(it => "box" in it
24
- ? {
25
- box: it.box,
26
- grow: it.grow ?? 1,
27
- shrink: it.shrink ?? 1,
28
- min: it.min ?? 0,
29
- max: it.max ?? Number.POSITIVE_INFINITY,
30
- }
31
- : { box: it, grow: 1, shrink: 1, min: 0, max: Number.POSITIVE_INFINITY });
32
- }
33
- /** Horizontal CSS-flex stack: per-item grow/shrink/min/max along the
34
- * main axis, cross-axis handled by `align`. */
35
- export function hstack(c, items, opts = {}) {
36
- return _stack(c, items, opts, "horizontal");
37
- }
38
- /** Vertical stack — top-to-bottom version of `hstack`. */
39
- export function vstack(c, items, opts = {}) {
40
- return _stack(c, items, opts, "vertical");
41
- }
42
- function _stack(c, rawItems, opts, dir) {
43
- const items = specs(rawItems);
44
- const horizontal = dir === "horizontal";
45
- const mainPos = (b) => asW(horizontal ? b.x : b.y);
46
- const mainSize = (b) => asW(horizontal ? b.w : b.h);
47
- const crossPos = (b) => asW(horizontal ? b.y : b.x);
48
- const crossSize = (b) => asW(horizontal ? b.h : b.w);
49
- const mode = opts.mode ?? "fit";
50
- const alignKind = opts.align ?? "start";
51
- const reads = [
52
- mainPos(c),
53
- mainSize(c),
54
- crossPos(c),
55
- crossSize(c),
56
- ...items.map(it => mainSize(it.box)),
57
- ...items.map(it => crossSize(it.box)),
58
- ...readDeps(opts.gap ?? 0, opts.padding ?? 0),
59
- ];
60
- const writes = [];
61
- for (const it of items) {
62
- writes.push(asW(mainPos(it.box)));
63
- if (mode === "fit")
64
- writes.push(asW(mainSize(it.box)));
65
- writes.push(asW(crossPos(it.box)));
66
- if (alignKind === "stretch")
67
- writes.push(asW(crossSize(it.box)));
68
- }
69
- return propagator(reads, writes, () => {
70
- const gap = readNow(opts.gap ?? 0);
71
- const pad = readNow(opts.padding ?? 0);
72
- const n = items.length;
73
- let sizes;
74
- if (mode === "fit") {
75
- // Start each item at its hypothetical size (current main-size,
76
- // clamped to [min, max]).
77
- sizes = new Array(n);
78
- let sumW = 0;
79
- for (let i = 0; i < n; i++) {
80
- const it = items[i];
81
- const w0 = clamp(mainSize(it.box).value, it.min, it.max);
82
- sizes[i] = w0;
83
- sumW += w0;
84
- }
85
- const slack = mainSize(c).value - 2 * pad - (n - 1) * gap - sumW;
86
- if (slack > 1e-9) {
87
- // Grow.
88
- let remaining = slack;
89
- const eligible = new Set();
90
- for (let i = 0; i < n; i++) {
91
- if (items[i].grow > 0 && sizes[i] < items[i].max)
92
- eligible.add(i);
93
- }
94
- while (remaining > 1e-9 && eligible.size > 0) {
95
- let weights = 0;
96
- for (const i of eligible)
97
- weights += items[i].grow;
98
- if (weights === 0)
99
- break;
100
- let absorbed = 0;
101
- for (const i of [...eligible]) {
102
- const share = (remaining * items[i].grow) / weights;
103
- const newW = Math.min(sizes[i] + share, items[i].max);
104
- absorbed += newW - sizes[i];
105
- sizes[i] = newW;
106
- if (newW >= items[i].max)
107
- eligible.delete(i);
108
- }
109
- if (absorbed < 1e-9)
110
- break;
111
- remaining -= absorbed;
112
- }
113
- }
114
- else if (slack < -1e-9) {
115
- // Shrink.
116
- let remaining = -slack;
117
- const eligible = new Set();
118
- for (let i = 0; i < n; i++) {
119
- if (items[i].shrink > 0 && sizes[i] > items[i].min)
120
- eligible.add(i);
121
- }
122
- while (remaining > 1e-9 && eligible.size > 0) {
123
- let weights = 0;
124
- for (const i of eligible)
125
- weights += items[i].shrink;
126
- if (weights === 0)
127
- break;
128
- let absorbed = 0;
129
- for (const i of [...eligible]) {
130
- const share = (remaining * items[i].shrink) / weights;
131
- const newW = Math.max(sizes[i] - share, items[i].min);
132
- absorbed += sizes[i] - newW;
133
- sizes[i] = newW;
134
- if (newW <= items[i].min)
135
- eligible.delete(i);
136
- }
137
- if (absorbed < 1e-9)
138
- break;
139
- remaining -= absorbed;
140
- }
141
- }
142
- }
143
- else {
144
- // hug: item sizes drive container.
145
- sizes = items.map(it => clamp(mainSize(it.box).value, it.min, it.max));
146
- let total = 2 * pad + (n - 1) * gap;
147
- for (const s of sizes)
148
- total += s;
149
- mainSize(c).value = total;
150
- }
151
- // Place items along main axis.
152
- let cursor = mainPos(c).value + pad;
153
- for (let i = 0; i < n; i++) {
154
- mainPos(items[i].box).value = cursor;
155
- if (mode === "fit")
156
- mainSize(items[i].box).value = sizes[i];
157
- cursor += sizes[i] + gap;
158
- }
159
- // Cross-axis alignment.
160
- const cBase = crossPos(c).value + pad;
161
- const cAvail = crossSize(c).value - 2 * pad;
162
- for (const it of items) {
163
- const itSize = crossSize(it.box).value;
164
- switch (alignKind) {
165
- case "start":
166
- crossPos(it.box).value = cBase;
167
- break;
168
- case "center":
169
- crossPos(it.box).value = cBase + (cAvail - itSize) / 2;
170
- break;
171
- case "end":
172
- crossPos(it.box).value = cBase + cAvail - itSize;
173
- break;
174
- case "stretch":
175
- crossPos(it.box).value = cBase;
176
- crossSize(it.box).value = cAvail;
177
- break;
178
- }
179
- }
180
- });
181
- }
182
13
  /** Regular grid: items placed in a `cols`-wide grid. Cells equal-size,
183
14
  * computed from container minus padding and gaps. */
184
15
  export function grid(c, items, opts) {
@@ -0,0 +1,17 @@
1
+ import { type Interval, type LatticeCell } from "./lattice.js";
2
+ import { type Propagator } from "./solver.js";
3
+ type I = LatticeCell<Interval>;
4
+ /** `x ∈ [lo, hi]`. Self-applying, so a widening write gets re-narrowed. */
5
+ export declare function bound(x: I, lo: number, hi: number): Propagator;
6
+ /** Pin `x` to a single value. */
7
+ export declare function fix(x: I, v: number): Propagator;
8
+ /** `a = b`. Each side narrows to the intersection, so order is irrelevant. */
9
+ export declare function equal(a: I, b: I): Propagator[];
10
+ /** `a + gap ≤ b`. Narrows `a` from above and `b` from below. */
11
+ export declare function order(a: I, b: I, gap?: number): Propagator[];
12
+ /** `a + b = c`. Three narrowers; any two bound the third. */
13
+ export declare function add(a: I, b: I, c: I): Propagator[];
14
+ /** `Σ parts = whole`. N+1 narrowers: whole from the parts, and each part
15
+ * from whole minus the others. Order-independent. */
16
+ export declare function total(parts: readonly I[], whole: I): Propagator[];
17
+ export {};
@@ -0,0 +1,93 @@
1
+ // numeric.ts — the handful of interval atoms.
2
+ //
3
+ // Every relation here narrows interval cells via `merge` and nothing
4
+ // else, so each is monotone and the whole set composes into bigger
5
+ // relations that inherit termination and order-independence for free.
6
+ //
7
+ // These are the primitives layout and graph layering desugar to:
8
+ // bound — x ∈ [lo, hi]
9
+ // equal — a = b (alignment)
10
+ // order — a + gap ≤ b (spacing / ordering / packing / layering)
11
+ // add — a + b = c (offsets, content sizes)
12
+ // total — Σ parts + slack = whole (flex main axis, distribution)
13
+ import { merge } from "./lattice.js";
14
+ import { propagator } from "./solver.js";
15
+ const ninf = Number.NEGATIVE_INFINITY;
16
+ const pinf = Number.POSITIVE_INFINITY;
17
+ /** `x ∈ [lo, hi]`. Self-applying, so a widening write gets re-narrowed. */
18
+ export function bound(x, lo, hi) {
19
+ return propagator([x], [x], () => {
20
+ merge(x, [lo, hi]);
21
+ });
22
+ }
23
+ /** Pin `x` to a single value. */
24
+ export function fix(x, v) {
25
+ return bound(x, v, v);
26
+ }
27
+ /** `a = b`. Each side narrows to the intersection, so order is irrelevant. */
28
+ export function equal(a, b) {
29
+ return [
30
+ propagator([a], [b], () => {
31
+ merge(b, a.value);
32
+ }),
33
+ propagator([b], [a], () => {
34
+ merge(a, b.value);
35
+ }),
36
+ ];
37
+ }
38
+ /** `a + gap ≤ b`. Narrows `a` from above and `b` from below. */
39
+ export function order(a, b, gap = 0) {
40
+ return [
41
+ propagator([b], [a], () => {
42
+ merge(a, [ninf, b.value[1] - gap]);
43
+ }),
44
+ propagator([a], [b], () => {
45
+ merge(b, [a.value[0] + gap, pinf]);
46
+ }),
47
+ ];
48
+ }
49
+ /** `a + b = c`. Three narrowers; any two bound the third. */
50
+ export function add(a, b, c) {
51
+ return [
52
+ propagator([a, b], [c], () => {
53
+ merge(c, [a.value[0] + b.value[0], a.value[1] + b.value[1]]);
54
+ }),
55
+ propagator([a, c], [b], () => {
56
+ merge(b, [c.value[0] - a.value[1], c.value[1] - a.value[0]]);
57
+ }),
58
+ propagator([b, c], [a], () => {
59
+ merge(a, [c.value[0] - b.value[1], c.value[1] - b.value[0]]);
60
+ }),
61
+ ];
62
+ }
63
+ /** `Σ parts = whole`. N+1 narrowers: whole from the parts, and each part
64
+ * from whole minus the others. Order-independent. */
65
+ export function total(parts, whole) {
66
+ if (parts.length === 0)
67
+ return [];
68
+ const props = [
69
+ propagator(parts, [whole], () => {
70
+ let lo = 0;
71
+ let hi = 0;
72
+ for (const p of parts) {
73
+ lo += p.value[0];
74
+ hi += p.value[1];
75
+ }
76
+ merge(whole, [lo, hi]);
77
+ }),
78
+ ];
79
+ for (let i = 0; i < parts.length; i++) {
80
+ const target = parts[i];
81
+ const others = parts.filter((_, j) => j !== i);
82
+ props.push(propagator([whole, ...others], [target], () => {
83
+ let oLo = 0;
84
+ let oHi = 0;
85
+ for (const o of others) {
86
+ oLo += o.value[0];
87
+ oHi += o.value[1];
88
+ }
89
+ merge(target, [whole.value[0] - oHi, whole.value[1] - oLo]);
90
+ }));
91
+ }
92
+ return props;
93
+ }
@@ -0,0 +1,51 @@
1
+ import { type Cell } from "../core/index.js";
2
+ type AnyCell = Cell<any>;
3
+ /** Reads/writes declare the topology; `step()` narrows the writes. */
4
+ export interface Propagator {
5
+ readonly reads: readonly Cell<any>[];
6
+ readonly writes: readonly Cell<any>[];
7
+ step(): void;
8
+ }
9
+ /** Plain-object propagator: no new cell type required to participate. */
10
+ export declare function propagator(reads: readonly Cell<any>[], writes: readonly Cell<any>[], step: () => void): Propagator;
11
+ export interface SolverOpts {
12
+ /** Safety bound on fixpoint waves for cyclic real-interval descent
13
+ * that converges only in the limit. Default 10_000. Finite lattices
14
+ * reach a fixpoint long before this. Hitting it stops with a sound
15
+ * result — it never throws. */
16
+ maxWaves?: number;
17
+ /** Don't auto-run on read changes; advance via `.step()`. For animated
18
+ * solvers and narrowing visualisations. */
19
+ manual?: boolean;
20
+ }
21
+ export declare class Solver {
22
+ private readonly _entries;
23
+ private readonly _maxWaves;
24
+ private readonly _manual;
25
+ private _network?;
26
+ private _firstFire;
27
+ private _fresh;
28
+ /** True iff the last drain hit `maxWaves` without reaching a fixpoint
29
+ * (still sound, just not fully converged). */
30
+ stalled: boolean;
31
+ constructor(opts?: SolverOpts);
32
+ /** Register propagators (arrays from combinators may be spread). Each
33
+ * first-fires once; later waves are freshness-gated. */
34
+ add(...props: readonly (Propagator | readonly Propagator[])[]): this;
35
+ /** Advance the fixpoint up to `waves` passes (default: to convergence).
36
+ * Meaningful in `manual` mode; auto mode drains inline. */
37
+ step(waves?: number): void;
38
+ /** Cells whose knowledge has collapsed to a contradiction. */
39
+ contradictions(): AnyCell[];
40
+ /** True iff any participating cell is a contradiction. */
41
+ get feasible(): boolean;
42
+ get count(): number;
43
+ dispose(): void;
44
+ private _addOne;
45
+ private _install;
46
+ private _drain;
47
+ }
48
+ /** A solver holding `props`. */
49
+ export declare function solve(...props: readonly (Propagator | readonly Propagator[])[]): Solver;
50
+ export declare function solver(opts?: SolverOpts): Solver;
51
+ export {};
@@ -0,0 +1,175 @@
1
+ // solver.ts — the monotone fixpoint engine.
2
+ //
3
+ // A propagator declares the cells it `reads` and `writes` and a `step()`
4
+ // that narrows its writes via `merge()`. The solver runs them to a
5
+ // fixpoint, freshness-gated: only propagators whose reads narrowed in
6
+ // the prior wave re-run.
7
+ //
8
+ // Termination is a property of the lattice, not a fuel cap. Finite-set
9
+ // cells shrink to a floor; interval cells shrink toward a point. So a
10
+ // run either reaches a fixpoint or (for cyclic real-interval descent
11
+ // that only converges in the limit) stops at `maxWaves` holding a SOUND
12
+ // over-approximation — never a wrong or oscillating value. There is no
13
+ // `DivergedError`: monotone narrowing cannot diverge, only slow-converge.
14
+ //
15
+ // Reads are expanded transitively at install time (via `transitiveDeps`)
16
+ // so a propagator reading a lens chain also re-fires when a parent of
17
+ // that chain narrows — no silent freshness gaps.
18
+ import { network as makeNetwork, transitiveDeps } from "../core/index.js";
19
+ import { isContradiction } from "./lattice.js";
20
+ /** Plain-object propagator: no new cell type required to participate. */
21
+ export function propagator(
22
+ // biome-ignore lint/suspicious/noExplicitAny: see header
23
+ reads,
24
+ // biome-ignore lint/suspicious/noExplicitAny: see header
25
+ writes, step) {
26
+ return { reads, writes, step };
27
+ }
28
+ export class Solver {
29
+ _entries = [];
30
+ _maxWaves;
31
+ _manual;
32
+ _network;
33
+ _firstFire = [];
34
+ _fresh = new Set();
35
+ /** True iff the last drain hit `maxWaves` without reaching a fixpoint
36
+ * (still sound, just not fully converged). */
37
+ stalled = false;
38
+ constructor(opts = {}) {
39
+ this._maxWaves = opts.maxWaves ?? 10_000;
40
+ this._manual = opts.manual ?? false;
41
+ }
42
+ /** Register propagators (arrays from combinators may be spread). Each
43
+ * first-fires once; later waves are freshness-gated. */
44
+ add(...props) {
45
+ const start = this._entries.length;
46
+ const newDeps = new Set();
47
+ for (const p of props) {
48
+ if (Array.isArray(p))
49
+ for (const pp of p)
50
+ this._addOne(pp, newDeps);
51
+ else
52
+ this._addOne(p, newDeps);
53
+ }
54
+ for (let i = start; i < this._entries.length; i++)
55
+ this._firstFire.push(this._entries[i].p);
56
+ if (this._network === undefined) {
57
+ this._install();
58
+ }
59
+ else {
60
+ this._network.subscribe(...newDeps);
61
+ this._network.flush();
62
+ }
63
+ return this;
64
+ }
65
+ /** Advance the fixpoint up to `waves` passes (default: to convergence).
66
+ * Meaningful in `manual` mode; auto mode drains inline. */
67
+ step(waves = this._maxWaves) {
68
+ if (this._network === undefined)
69
+ return;
70
+ this._network.flush();
71
+ this._drain(waves);
72
+ }
73
+ /** Cells whose knowledge has collapsed to a contradiction. */
74
+ contradictions() {
75
+ const out = new Set();
76
+ for (const { p } of this._entries) {
77
+ for (const w of p.writes)
78
+ if (isContradiction(w))
79
+ out.add(w);
80
+ for (const r of p.reads)
81
+ if (isContradiction(r))
82
+ out.add(r);
83
+ }
84
+ return [...out];
85
+ }
86
+ /** True iff any participating cell is a contradiction. */
87
+ get feasible() {
88
+ return this.contradictions().length === 0;
89
+ }
90
+ get count() {
91
+ return this._entries.length;
92
+ }
93
+ dispose() {
94
+ this._network?.dispose();
95
+ this._network = undefined;
96
+ }
97
+ _addOne(p, newDeps) {
98
+ const expanded = expandReads(p.reads);
99
+ this._entries.push({ p, expanded });
100
+ for (const s of expanded)
101
+ newDeps.add(s);
102
+ }
103
+ _install() {
104
+ const allDeps = new Set();
105
+ for (const { expanded } of this._entries)
106
+ for (const s of expanded)
107
+ allDeps.add(s);
108
+ this._network = makeNetwork([...allDeps], dirty => {
109
+ for (const p of this._firstFire) {
110
+ for (const w of runPropagator(p))
111
+ this._fresh.add(w);
112
+ }
113
+ this._firstFire = [];
114
+ for (const s of dirty)
115
+ this._fresh.add(s);
116
+ if (!this._manual)
117
+ this._drain(this._maxWaves);
118
+ }, { manual: this._manual });
119
+ }
120
+ _drain(maxWaves) {
121
+ if (this._entries.length === 0)
122
+ return;
123
+ let waves = 0;
124
+ while (this._fresh.size > 0 && waves < maxWaves) {
125
+ waves++;
126
+ const fresh = this._fresh;
127
+ this._fresh = new Set();
128
+ for (const { p, expanded } of this._entries) {
129
+ if (!intersects(expanded, fresh))
130
+ continue;
131
+ for (const w of runPropagator(p))
132
+ this._fresh.add(w);
133
+ }
134
+ }
135
+ // Sound either way: leftover fresh just means more narrowing was
136
+ // available than `maxWaves` allowed. Flag it; never throw.
137
+ this.stalled = this._fresh.size > 0;
138
+ if (this.stalled)
139
+ this._fresh = new Set();
140
+ }
141
+ }
142
+ /** Expand declared reads to include transitive (lens-chain) parents. */
143
+ function expandReads(reads) {
144
+ const out = new Set();
145
+ for (const r of reads)
146
+ for (const dep of transitiveDeps(r))
147
+ out.add(dep);
148
+ return [...out];
149
+ }
150
+ /** Run `step()`; return the writes whose value actually changed. */
151
+ function runPropagator(p) {
152
+ const before = new Array(p.writes.length);
153
+ for (let i = 0; i < p.writes.length; i++)
154
+ before[i] = p.writes[i].peek();
155
+ p.step();
156
+ const changed = new Set();
157
+ for (let i = 0; i < p.writes.length; i++) {
158
+ if (p.writes[i].peek() !== before[i])
159
+ changed.add(p.writes[i]);
160
+ }
161
+ return changed;
162
+ }
163
+ function intersects(expanded, fresh) {
164
+ for (const r of expanded)
165
+ if (fresh.has(r))
166
+ return true;
167
+ return false;
168
+ }
169
+ /** A solver holding `props`. */
170
+ export function solve(...props) {
171
+ return new Solver().add(...props);
172
+ }
173
+ export function solver(opts = {}) {
174
+ return new Solver(opts);
175
+ }
@@ -0,0 +1 @@
1
+ export { addField, addV, each, eachBy, type FieldMap, headField, headV, inField, into, mapElems, mapField, nestFields, nestV, type Obj, type OLens, onField, pipe, recurse, removeField, removeV, renameField, renameV, type SplitSpec, type Step, seq, splitField, splitV, toStep, type VLens, wrapField, wrapV, } from "./lens.js";
@@ -0,0 +1,3 @@
1
+ // schema — composable, complement-carrying lenses between plain-POJO schema
2
+ // versions. See lens.ts for the kit and the design rationale.
3
+ export { addField, addV, each, eachBy, headField, headV, inField, into, mapElems, mapField, nestFields, nestV, onField, pipe, recurse, removeField, removeV, renameField, renameV, seq, splitField, splitV, toStep, wrapField, wrapV, } from "./lens.js";