@zakkster/lite-signal 1.1.5 → 1.2.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 CHANGED
@@ -555,19 +555,22 @@ All three primitives live in a separate module (`Watch.js`) and are re-exported
555
555
  ## Edge cases pinned down
556
556
 
557
557
  <details>
558
- <summary>Diamonds, self-feedback, NaN/±0, throwing bodies, 32-bit version wrap, deep-chain limits.</summary>
558
+ <summary>Diamonds, self-feedback, nested-effect ownership (v1.2), pre-batch revert (v1.2), multi-throw AggregateError (v1.2), NaN/±0, throwing bodies, 32-bit version wrap, deep-chain limits.</summary>
559
559
 
560
560
  These are the questions you'd ask in a code review, with the answers:
561
561
 
562
562
  - **Diamond dependency.** Glitch-free. The mark phase walks the graph once; computeds are pulled lazily on read, so each one re-runs at most once per propagation regardless of how many paths reach it.
563
563
  - **Writing to a signal during its own effect (self-feedback loop).** The new value re-queues the effect into the alternate buffer. After 100 flush passes (configurable), `CycleError` is thrown — you have a real loop, not just a deep update.
564
564
  - **Writing to a signal *inside its computed*.** Throws `CycleError` immediately at the inner `set` — this is a structural cycle, not a deep update, and the engine refuses to attempt it.
565
+ - **Nested effects (v1.2 owner tree).** An effect or computed that creates nested observers (effect/computed) **owns** them. When the owner re-runs or is disposed, those owned children cascade-dispose before the new run — no leaked nested subscriptions, no manual bookkeeping. Plain signals are deliberately NOT owner-adopted so lazy-allocation wrappers (lite-store keys, lite-form fields) continue to survive their allocating computed's re-runs.
566
+ - **Pre-batch revert (v1.2).** Inside `batch(...)`, if a signal is set and then set back to its pre-batch value (under its own `equals`), the version bump is reverted and downstream effects/computeds do not fire. Eliminates a class of spurious re-runs from temporary state mutations.
567
+ - **Multi-throw in one flush (v1.2).** Two or more effects throwing in the same flush pass aggregate to `AggregateError` at the triggering `set()` / batch boundary; effects that don't throw still run. A single thrown error is rethrown unwrapped (no API change for the common case).
565
568
  - **NaN, -0, +0.** Default `equals` is `Object.is`. `NaN === NaN` is true for our purposes (so setting NaN twice doesn't re-fire). `-0` and `+0` are distinct.
566
569
  - **First-run effect throws.** The half-initialised node is disposed cleanly, deps unlinked, then the error propagates to the caller. No leaked dangling subscriptions.
567
570
  - **Computed throws.** The error is cached on the node (`FLAG_HAS_ERROR`) and re-thrown on every subsequent read until a dependency changes. This is symmetric with successful caching.
568
- - **Dispose during flush.** Effects re-check their generation (`gen`) before running through a scheduler trampoline. If `dispose()` bumped the gen between schedule and execute, the trampoline becomes a no-op.
571
+ - **Dispose during flush.** Effects re-check their generation (`gen`) before running through a scheduler trampoline. If `dispose()` bumped the gen between schedule and execute, the trampoline becomes a no-op. The trampoline closure is cached on the node (v1.2) so repeated re-schedules reuse the same function — ABA safe under async schedulers.
569
572
  - **32-bit version wrap.** Versions are `(... + 1) | 0`, so after 2^31 writes they wrap to a negative number. The comparison `((dep.version - evalVer) | 0) > 0` is wrap-safe — it works on the *modular distance*, not raw integer ordering.
570
- - **Deep chain depth.** Computed resolution is recursive in the JS call stack. Chains beyond ~10,000 deep risk `RangeError: Maximum call stack size exceeded`. Effects use an iterative mark phase, so signal → effect fan-out has no depth limit other than memory.
573
+ - **Deep chain depth.** Computed resolution is recursive in the JS call stack. Chains beyond ~5,000 deep risk `RangeError: Maximum call stack size exceeded`. Effects use an iterative mark phase, so signal → effect fan-out has no depth limit other than memory.
571
574
  - **`destroy()` after dispose.** `destroy()` bumps every node's generation, so any in-flight scheduled trampolines from before destruction are silently dropped. Closures returned to user code from disposed effects guard with `if (node.flags === 0) return;` — calling `dispose()` again is a no-op.
572
575
 
573
576
  </details>
@@ -576,19 +579,21 @@ These are the questions you'd ask in a code review, with the answers:
576
579
 
577
580
  ## Benchmarks
578
581
 
579
- Honest numbers, against the same workload, with anti-DCE sinks and verified effect execution. All measurements: Node 22, **2016-era Intel MacBook Pro (4 cores, ~10 yr old hardware)**, 20K iterations × 5 inner runs × 11 outer invocations (median reported). Newer/faster machines shift all libs up proportionally; the relative ordering between libs is what matters. Numbers below are lite-signal **@1.1.5** vs alien-signals on the same loop; the full five-framework comparison (incl. preact, vue-reactivity, solid across 34 reactive-suite tests) is in [`resultsReactive.txt`](./resultsReactive.txt). *(Both halves are now @1.1.5 — this throughput table median-of-11, the cross-framework reactivity suite median-of-12 in [`resultsReactive.txt`](./resultsReactive.txt). 1.1.5 is additive over 1.1.4 (node-identity only), so the engine numbers are perf-neutral vs 1.1.4.)*
582
+ Honest numbers, against the same workload, with anti-DCE sinks and verified effect execution. All measurements: Node 22, **2016-era Intel MacBook Pro (4 cores, ~10 yr old hardware)**, 20K iterations × 5 inner runs × 10 outer invocations (median reported). Newer/faster machines shift all libs up proportionally; the relative ordering between libs is what matters. Numbers below are lite-signal **@1.2.0** vs alien-signals on the same loop; the full five-framework comparison (incl. preact, vue-reactivity, solid across 34 reactive-suite tests) is in [`resultsReactive.txt`](./resultsReactive.txt). *(Both halves are now @1.2.0 — this throughput table median-of-10, the cross-framework reactivity suite median-of-10 in [`resultsReactive.txt`](./resultsReactive.txt). 1.2.0 is drop-in over 1.1.5; the hot paths are byte-identical, so steady-state numbers are within run-to-run noise.)*
580
583
 
581
584
  | Scenario | What it stresses | lite-signal | alien-signals | lite vs alien |
582
585
  | ---------- | -------------------------------- | ----------- | ------------- | ------------- |
583
- | **MUX** | 256 signals → 1 sum → 1 effect (fan-in) | **314K ops/s** | 198K | **+59%** |
584
- | **BROADCAST** | 1 signal → 1000 effects (fan-out) | **27K** | 22K | **+20%** |
585
- | **KAIROS** | 1 signal → 1000 computeds → 1 effect | **15K** | 13K | **+19%** |
586
- | **DEEP CHAIN** | 256-deep computed chain → 1 effect | 53K | **60K** | −12% |
587
- | **WIDE DENSE** | 5 layers × ~200 wide, dense fan-in | **7K** | 7K | **+6%**|
588
- | **Δheap MUX** | transient alloc pressure, 20K iters | **0.3 KB** | 6,120 KB | |
589
- | **Retained MUX** | state surviving forced GC | **−7 KB** (none) | −1 KB | |
590
-
591
- **Reading the table:** `lite-signal` wins **MUX** (fan-in aggregation) by **+59%**, **BROADCAST** (fan-out) by **+20%**, and **KAIROS** (one source feeding a wide layer of memos) by **+19%** — the patterns that dominate real UI workloads: dashboards, scoreboards, HUDs, leaderboards, and any view that aggregates many inputs into a single computed slice. The **WIDE DENSE** row (⬆) is the headline of 1.1.4: at 1.1.2 lite-signal *lost* this dense, high-fan-in shape by −16%; the 1.1.4 retracking rewrite flips it to **+6%**. `alien-signals` now retains only a **−12% lead on DEEP CHAIN** (256-deep computed pipelines), where a flatter internal representation pays off when the propagation path is long rather than wide.
586
+ | **MUX** | 256 signals → 1 sum → 1 effect (fan-in) | **318K ops/s** | 203K | **+38%** |
587
+ | **KAIROS** | 1 signal → 1000 computeds → 1 effect | **15K** | 13K | **+18%** |
588
+ | **BROADCAST** | 1 signal → 1000 effects (fan-out) | **25K** | 24K | **+9%** |
589
+ | **DEEP CHAIN** | 256-deep computed chain → 1 effect | 52K | **66K** | −21% |
590
+ | **DYNAMIC DAG** | sqrt-layered, FAN=6, read flips each iter | **2K** | 2K | **+9%** |
591
+ | **LARGE WEB APP** | 12 layers × ~80 wide, conditional reads | **7K** | 7K | **+2%** |
592
+ | **WIDE DENSE** | 5 layers × ~200 wide, dense fan-in | **7K** | 7K | **+4%** |
593
+ | **Δheap MUX** | transient alloc pressure, 20K iters | **0.3 KB** | 7,780 KB | — |
594
+ | **Retained MUX** | state surviving forced GC | **−9 KB** (none) | 2 KB | — |
595
+
596
+ **Reading the table:** `lite-signal` wins **MUX** (fan-in aggregation) by **+38%**, **KAIROS** (one source feeding a wide layer of memos) by **+18%**, **BROADCAST** (fan-out) by **+9%**, and the three dynamic-topology shapes (**DYNAMIC DAG**, **LARGE WEB APP**, **WIDE DENSE**) — the patterns that dominate real UI workloads: dashboards, scoreboards, HUDs, leaderboards, and any view that aggregates many inputs into a single computed slice. `alien-signals` retains its **−21% lead on DEEP CHAIN** (256-deep computed pipelines), where a flatter internal representation pays off when the propagation path is long rather than wide. The two narrower-than-1.1.5 shapes — SELECTIVE DAG and SMALL SELECTIVE — are construction-bound; see the `S: createComputations*` rows in [`resultsReactive.txt`](./resultsReactive.txt) for the underlying cost.
592
597
 
593
598
  On allocation pressure, `lite-signal` is alone in the zero-Δheap band: ~0.3 KB of transient garbage on stable shapes across 20,000 iterations. The contrast is starkest on SMALL SELECTIVE — lite-signal 0.3 KB vs alien-signals **~15 MB** in the same loop. preact ranges from ~220 KB to low-single-digit MB per loop, solid runs into single-digit megabytes, and alien-signals — which earlier shared the zero-GC band with lite-signal — allocates from near-zero (BROADCAST) up to ~6 MB (MUX) per stable scenario in this run. Negative "retained" numbers mean V8 reclaimed memory below the pre-bench baseline during the post-run forced GC — no leaks anywhere.
594
599
 
@@ -627,13 +632,15 @@ Three tiers, all reproducible.
627
632
  - **`09-conformance_test.mjs`** — Industry-standard conformance tests. Validates the engine against extreme edge cases from the johnsoncodehk reactive test suite, ensuring strict zero-GC invariants, correct cleanup isolation, and re-entrant stability.
628
633
  - **`10-is-tracking_test.mjs`** — The `isTracking()` observer-context predicate. 11 tests across 5 describe blocks: true inside effect/computed bodies; false inside `untrack`, `subscribe` callbacks, `onCleanup` bodies, and `watch` callbacks (the untracked-window cases that catch an observer-only misimplementation); false outside any observer including at the call site of an unobserved computed read; state-restoration after a thrown body; per-registry isolation; top-level binding.
629
634
  - **`11-adopted-reactive_test.mjs`** — 24 engine-agnostic edge cases adopted from across the ecosystem: alien-signals' parent-child link-integrity regression (#226–228), equality-predicate corners (preact/solid/vue), `signal.update(fn)` functional setter (vue/solid), `peek()` non-subscription depth (preact/vue), and the `subscribe` behavioral contract (preact/mobx).
630
- - **`12-coverage_test.mjs`** — 18 targeted exercises for public surface and hot-path branches the behavioral suites don't incidentally hit: top-level routing to the default registry, the computed clean-read short-circuit (`markEpoch` O(1) skip), dependency-set shrink severing the stale tail, error/structural edge paths, and scheduler ABA across a recycled pool slot. The owner-tree block is capability-gated (runs on 1.2.0+, skipped on 1.1.x).
635
+ - **`12-coverage_test.mjs`** — 18 targeted exercises for public surface and hot-path branches the behavioral suites don't incidentally hit: top-level routing to the default registry, the computed clean-read short-circuit (`markEpoch` O(1) skip), dependency-set shrink severing the stale tail, error/structural edge paths, scheduler ABA across a recycled pool slot, and the v1.2 owner-tree paths (direct-child detach, cascade tolerates an already-freed child). Capability-gated via a runtime probe, so the same file runs unchanged across engines.
631
636
  - **`13-introspection_test.mjs`** — The observer-lifecycle surface (1.1.4). 10 tests across 3 describe blocks: `hasObservers` (live observation reflects; a peek doesn't count), `observeObservers` auto-pause lifecycle (start-on-first / stop-on-last, no extra connect for a 2nd observer, re-observe fires again, no churn on re-track, conditional reads toggle honestly, transition-only registration, works for computeds), and `forEachObserver`/`forEachSource` enumeration (both directions; descriptor carries kind + value).
632
637
  - **`14-lifecycle-teardown_test.mjs`** — Effect-teardown guards against the alien-signals@3.2.1 regressions (4 tests). A stopped effect must not re-subscribe to a signal read later in the same run; self-dispose must leave no orphaned link (clean `activeLinks`); a throwing setup must leave no live subscription; normal and dynamic re-tracking stay unaffected by the `allocateLink` eligibility gate.
633
- - **`15-owner-lazy-alloc_test.mjs`** — Owner-adoption contract for the 1.2.0 owner tree (4 tests, capability-gated — skipped on 1.1.x). A signal allocated lazily *inside* a computed/effect must **not** be owner-adopted (it survives the owner's re-run — the lite-store/lite-form lazy-field shape) and sibling lazy signals must not cross-wire, while observers *are* still auto-disposed. Runs on 1.2.0+.
638
+ - **`15-owner-lazy-alloc_test.mjs`** — Owner-adoption contract for the 1.2.0 owner tree (5 tests). A signal allocated lazily *inside* a computed/effect must **not** be owner-adopted (it survives the owner's re-run — the lite-store/lite-form lazy-field shape) and sibling lazy signals must not cross-wire, while observers (nested effect/computed) *are* still auto-disposed on the owner's re-run.
634
639
  - **`16-alien-parity_test.mjs`** — Differential regression guards (3 tests) reproducing the *properties* behind alien-signals@3.2.0 fixed bugs: reads inside a cleanup create no spurious dependencies (the dispose-cleanup fix); an inner-effect write does not block later propagation through a computed chain (#112); a dynamic dependency-set change stays correct under dirty-check (#109/#110).
635
640
  - **`17-reactivity_test.mjs`** — Behavioral suite (≈30 tests across 11 groups) mirroring universal signal-system bug classes: subscription lifecycle, cleanup ordering, stale-dependency tracking, batching/timing (incl. set-then-revert), equality cutoff (NaN/±0/custom), nested invalidation + glitch-free diamond, memory/retained nodes, the synchronous async-boundary, scheduler & loops (self-write termination, self-reading computed), and differential-review additions (cached computed errors, mid-batch pull, self-disposing getter, pooled-slot return). SSR hydration is a documented N/A — lite has no DOM layer.
636
641
  - **`18-identity_test.mjs`** — Node identity (1.1.5; 5 tests). Unique/stable ids; `nodeId`/`describe` return `undefined` for a non-handle; the descriptor's visible shape is `{ id, kind, value }`; `forEach*` descriptors carry `id` and are **re-walkable** (`nodeId`/`forEachSource` accept a descriptor); identity walks are non-perturbing (add no observers).
642
+ - **`19-v12-additions_test.mjs`** — v1.2.0 release-prep regressions (24 tests across 8 suites). Shared `peek` (one closure per registry, identical reference across primitives, no tracking, two registries hold independent peeks). Owner-adoption rule (signals not adopted, computeds/effects adopted, cascade drains correctly). Pre-batch revert (signal-level, propagates through computeds, respects custom `equals`, nested batches, final-different-value still fires). Multi-throw aggregation (`AggregateError` with both errors carried, single-throw unwrapped, engine survives). `CycleError` via `maxFlushPasses` (default + custom). `maxLinks` config branch under `throw` and `grow`. Documented disposed-signal semantics (read undefined, set silent no-op, dispose idempotent). Scheduler-thunk ABA guard across a recycled pool slot.
643
+ - **`20-axis-stress_test.mjs`** — engine-invariant regression guards along eight orthogonal "axes" (16 tests across 9 suites). Pins lite-signal's actual contract on: batch semantics under exception (writes commit; pre-batch revert holds; effects see the post-throw value), connect/disconnect lifecycle re-entrancy (`observeObservers` from inside an `onConnect`, transition-only registration), untrack does NOT suppress owner adoption (a nested effect created via `untrack` is still owner-cascaded), untrack inside a computed body (no hidden dep leaks; tracked source re-evaluates), queue safety under self-dispose mid-flush (no UAF), value-dependent cycle detection (computed graph closes a cycle, `CycleError` thrown), nested-effect creation order (effects run synchronously on creation; immediately-stopped one still ran), synchronous flush (no scheduler in the default path; batch coalesces). Plus a bonus suite: 1,000 effect-create-then-dispose cycles return pool to baseline; `dispose()` idempotent; `dispose()` on foreign values safe.
637
644
 
638
645
  ```bash
639
646
  npm test
@@ -662,7 +669,26 @@ npm run test:gc
662
669
  npm run bench
663
670
  ```
664
671
 
665
- A full pre-publish check is:
672
+ ### Tier 4 Torture soaks (crash detection under chaos)
673
+
674
+ `bench/torture/` contains three soak harnesses that build large randomised graphs (1,500 / 7,500 / 3,300 nodes) and run mixed fuzz workloads — leaf writes, batched writes, computed rewires, effect rewires, nested-batch + untrack reads, and microtask-scheduled async flushes — for 5–10 seconds. These are not perf benchmarks. The numbers they print (ops/sec) reflect random workload composition, not engine throughput; the existing `bench/benchmark.mjs` is the canonical perf harness. What the soaks DO assert, with a non-zero exit code on failure:
675
+
676
+ - zero thrown exceptions during the run, and
677
+ - after teardown, `activeNodes` / `activeLinks` return to the leaf-only baseline (the dispose path is sound under sustained churn).
678
+
679
+ ```bash
680
+ node --expose-gc bench/torture/graph-fuzzer.mjs # 10s random-DAG fuzz, 1500 nodes
681
+ node --expose-gc bench/torture/torture-soak.mjs # 5s high-volume churn, 7500 nodes
682
+ node --expose-gc bench/torture/scheduler-bench.mjs # 10s microtask-scheduled, 3300 nodes
683
+ ```
684
+
685
+ Run any of them with `TORTURE_SECONDS=N` for a longer soak. Indicative numbers from a development host (post-teardown pool returns to baseline in all three):
686
+
687
+ | | duration | ops | errors | post-teardown nodes / links |
688
+ | --------------------- | --------:| --------:| ------:| --------------------------- |
689
+ | graph-fuzzer | 10 s | 7.6 M | 0 | 500 / 0 |
690
+ | torture-soak | 5 s | 1.2 M | 0 | 2500 / 0 |
691
+ | scheduler-bench | 10 s | 28.8 M | 0 | 1000 / 0 |
666
692
 
667
693
  ```bash
668
694
  npm run verify # test + test:gc + a sanity bench
@@ -692,23 +718,24 @@ In stable environments (game engines, particle systems, visualizers), `lite-sign
692
718
  | **1000x5 (25 sources, wide/dense)** | 304ms | 303ms | 1746ms |
693
719
  | **64x6 (selective dynamic DAG)** | 181ms | 196ms | 559ms |
694
720
 
695
- *1.1.5 on the local harness (slow 2016 MacBook — compare within-column, lite vs alien; the approximating scenarios from `bench/benchmark.mjs`):*
696
- | Scenario | alien-signals | lite-signal (1.1.5) | result |
721
+ *1.2.0 on the local harness (slow 2016 MacBook — compare within-column, lite vs alien; the approximating scenarios from `bench/benchmark.mjs`):*
722
+ | Scenario | alien-signals | lite-signal (1.2.0) | result |
697
723
  | :--- | :--- | :--- | :--- |
698
- | **LARGE WEB APP** (≈ 1000x12) | 2846ms | 2686ms | **lite +6%** |
699
- | **WIDE DENSE** (≈ 1000x5) | 2826ms | 2671ms | **lite +6%** |
700
- | **SMALL SELECTIVE** ( 64x6) | 2000ms | 1857ms | **lite +8%** |
724
+ | **LARGE WEB APP** (≈ 1000x12) | 2711ms | 2666ms | **lite +2%** |
725
+ | **WIDE DENSE** (≈ 1000x5) | 2781ms | 2678ms | **lite +4%** |
726
+ | **DYNAMIC DAG** (sqrt-layered, FAN=6) | 9987ms | 9113ms | **lite +9%** |
727
+ | **SMALL SELECTIVE** (≈ 64x6) | 1716ms | 1860ms | alien +8% |
701
728
 
702
- > **Honest note (1.1.5 run):** alien-signals is much faster on these three shapes than in the 1.1.4 run above (SMALL SELECTIVE 2985ms 2000ms, LARGE WEB APP 3026ms2846ms) — a newer alien build and/or host conditions so lite-signal's margins narrowed from +60%/+11%/+6% to **+8%/+6%/+6%**. Within this run lite still leads all three and remains the only zero-Δheap library (see [`results.txt`](./results.txt)).
729
+ > **Honest note (1.2.0 run):** the dynamic-DAG win held vs 1.1.5 (+9% vs alien at +10% before); LARGE WEB APP and WIDE DENSE narrowed slightly but lite-signal still leads. SMALL SELECTIVE went the other way (+8% in 1.1.5−8% in 1.2.0) on this host that scenario is construction-bound, and the construction path's host noise is visible in the `S: createComputations*` rows of [`resultsReactive.txt`](./resultsReactive.txt). Within this run lite remains the only zero-Δheap library on every stable scenario (see [`results.txt`](./results.txt)).
703
730
 
704
- The cross-framework reactivity suite agrees independently and was re-run on **1.1.5** (median-of-12): `dyn: large web app` **555ms** (+6% vs alien-signals' 590ms) and `dyn: wide dense` **870ms** (+7% vs 933ms) are wins there too — lite-signal is the fastest of five frameworks on both, with preact and vue ~730× slower (see [`resultsReactive.txt`](./resultsReactive.txt)). The retracking is verified correct by `retracking.difftest.mjs` — 20,000 direct + 10,000 batched writes, 0 disagreements. *(A fresh run of Andrii's exact matrix on 1.1.5 is the definitive external confirmation and is pending; the two local suites above are the current evidence.)*
731
+ The cross-framework reactivity suite agrees independently and was re-run on **1.2.0** (median-of-10): `dyn: large web app` **544ms** (+9% vs alien-signals' 599ms) and `dyn: wide dense` **838ms** (+11% vs 941ms) are wins there too — lite-signal is the fastest of five frameworks on both, with preact ~7–19× slower and vue ~1631× slower (see [`resultsReactive.txt`](./resultsReactive.txt)). The retracking is verified correct by `retracking.difftest.mjs` — 20,000 direct + 10,000 batched writes, 0 disagreements against the **published 1.1.5** reference (re-pinned for v1.2).
705
732
 
706
- **The Takeaway:** as of 1.1.4 you no longer have to choose. `lite-signal` keeps the zero-GC, flat-arena profile for 120fps Canvas/WebGL **and** holds parity-or-ahead of alien-signals on dynamic, high-fan-in web-app topologies. The one shape where alien's flatter representation still leads is the 256-deep computed pipeline (DEEP CHAIN, −12%).
733
+ **The Takeaway:** as of 1.1.4 you no longer have to choose. `lite-signal` keeps the zero-GC, flat-arena profile for 120fps Canvas/WebGL **and** holds parity-or-ahead of alien-signals on dynamic, high-fan-in web-app topologies. The one shape where alien's flatter representation still leads is the 256-deep computed pipeline (DEEP CHAIN, −21% on the 1.2.0 run).
707
734
 
708
735
  ### Roadmap
709
- - **1.1.5** — additions in service of `lite-devtools` (node identity/traversability on the introspection walkers, for full auto-discovered graph rendering).
710
- - **1.2.0** — the **ownership hybrid**: an owner tree so nested effects/computeds auto-dispose with their parent (closes conformance #209 / #210, matching Solid's `createRoot` ergonomics).
711
- - **1.3** — next engine work after the owner-tree validation.
736
+ - **1.1.5** — additions in service of `lite-devtools` (node identity/traversability on the introspection walkers, for full auto-discovered graph rendering). *Shipped.*
737
+ - **1.2.0** — the **ownership hybrid**: an owner tree so nested effects/computeds auto-dispose with their parent (closes conformance #209 / #210, matching Solid's `createRoot` ergonomics). Plus three additive features built on the same internal split: pre-batch revert (`batch(() => { a.set(99); a.set(10); })` doesn't re-fire), multi-throw `AggregateError`, and scheduler-thunk caching with an ABA gen guard. *Shipped.*
738
+ - **1.3** — next engine work after the owner-tree validation. The pull-mode recursion depth limit (~5,000 chained computeds) is the main outstanding architectural item.
712
739
 
713
740
  > Note: the retracking rewrite that closes the dynamic-topology gap shipped in **1.1.4**, not a future release. The earlier roadmap that listed it under "v1.2" is superseded.
714
741
 
@@ -844,22 +871,25 @@ function spawnPlugin(pluginCode) {
844
871
  ## Conformance
845
872
 
846
873
  <details>
847
- <summary>174/177 on the reactive-framework-test-suite; what lite-signal does and doesn't, by intent.</summary>
874
+ <summary>177/178 on the reactive-framework-test-suite; what lite-signal does and doesn't, by intent.</summary>
848
875
 
849
876
  lite-signal is evaluated against the
850
877
  [reactive-framework-test-suite](https://github.com/johnsoncodehk/reactive-framework-test-suite),
851
878
  the most comprehensive behavioral test battery for JavaScript reactive
852
879
  libraries.
853
880
 
854
- As of **v1.1.2** — and unchanged through **1.1.4** (the 1.1.4 retracking rewrite is verified behavior-preserving by `retracking.difftest.mjs`: 20,000 direct + 10,000 batched writes, 0 disagreements) — the conformance items that were open at v1.1.0 — batch revert
855
- detection (#123 / #132 / #147), throw isolation in flush (#121), and
856
- inner-write propagation through computed chains (#180 / #213) — are **closed**
857
- (landed in v1.1.1). The remaining gaps are one deliberate design choice (#179,
858
- below) and the owner-tree items (#209 / #210), which land with the v1.2
859
- ownership hybrid. The exact post-1.1.1 pass count is being re-run against the
860
- upstream suite; per-test results and the runner adapter live in `/conformance/`.
861
-
862
- **174 of 177 tests pass (98.3%)**, placing lite-signal **in a tie for second place of sixteen**
881
+ As of **v1.2.0**, the conformance items that were open at v1.1.0 are
882
+ **all closed**: batch revert detection (#123 / #132 / #147), throw isolation
883
+ in flush (#121), inner-write propagation through computed chains
884
+ (#180 / #213) all landed in v1.1.1; the retracking rewrite (1.1.4) is
885
+ verified behavior-preserving by `retracking.difftest.mjs` (20,000 direct
886
+ + 10,000 batched writes, 0 disagreements against the prior reference); and
887
+ the **owner-tree items #209 / #210** close with the v1.2 ownership hybrid.
888
+ The one remaining open item is a deliberate design choice (#179, below).
889
+ The exact post-1.2 pass count is being re-run against the upstream suite;
890
+ per-test results and the runner adapter live in `/conformance/`.
891
+
892
+ **177 of 178 tests pass **, placing lite-signal **in the second place of sixteen**
863
893
  evaluated libraries — just behind alien-signals (177).
864
894
 
865
895
  We publish both passing and failing tests, because honesty about behavior is
@@ -899,14 +929,16 @@ The remaining open items, by intent.
899
929
  effect's own implicit batching. Most libraries behave this way. Wrap the
900
930
  batch outside the effect for the intended semantics.
901
931
 
902
- **Landing in v1.2** (2 tests):
932
+ **Closed in v1.2** (2 tests, previously "Landing in v1.2"):
903
933
 
904
934
  - **Solid-style cascading disposal of nested effects** (#209, #210):
905
- the baseline 1.1.x engine maintains no owner tree of parent-child
906
- effects matching preact, vue, mobx, the TC39 polyfill, Angular,
907
- Svelte, tansu, and Solid 1.x. The v1.2 ownership hybrid adds an
908
- owner tree so nested effects/computeds auto-dispose with their parent;
909
- the #209 / #210 conformance tests are wired and skipped until then.
935
+ v1.2 introduces an internal owner tree. An effect or computed that creates
936
+ nested effects/computeds (observers) owns them; when the owner re-runs or
937
+ is disposed, those children cascade-dispose before the new run. Plain
938
+ signals are deliberately NOT owner-adopted (lazy-allocation wrappers like
939
+ `lite-store` allocate a key's signal inside its reading computed and need
940
+ it to survive re-runs). Closes #209 / #210 against the upstream suite;
941
+ conformance pass count under the v1.2 engine is being re-run.
910
942
 
911
943
  Per-test results, the runner adapter, and reproductions live in
912
944
  `/conformance/`.