---
id: "packages/active-state/changelog"
title: "Changelog"
description: "What changed for users of active-state, per version."
url: "/packages/active-state/changelog"
locale: "en"
revision: "3cbc83b90f4d01e1"
---

# Changelog

> [!NOTE]
> **Release state**
>
> The version line at the top of this page is read from the package and the
> release register of Foundation. A version heading in the changelog below is
> not a release by itself: only a recorded release is. The section Unreleased
> lists what changed since the last version, including the changes made since
> Foundation maintains the package.

Notable changes to `@workspace/active-state`. Format based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning per
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

This file is the authoritative release history for `@workspace/active-state`.
The pre-1.0.0 entries (v0.1.0 → v0.2.12) document the internal iteration that
led to the v1.0.0 baseline and are preserved verbatim. That baseline is an
internal one: the package has never been published, and the readiness wording
in the `[1.0.0]` entry described an intent rather than a verified artifact.

---

## \[Unreleased\]

### Maintained in Foundation (since 2026-10-21)

- **Tests run without the source repository.** The one-engine evidence of
  the package (`src/__tests__/one-engine.test.ts`) and the adapter boundary
  read the package's own files with `node:fs` instead of the host's
  `repo-tools`. The census of a host repository's importers (every importer
  uses one of the three entry points and declares the package) scanned the
  host's workspaces, not the package; it is no longer part of the package
  and belongs with the host.
- **Documentation:** references to documents of the source repository are
  text, marked `(source repository: …)`, instead of links that resolve nowhere here.

### Changed — registered as a reusable product (Besluit 6)

- `package.json`: `@types/react` is an optional peer beside `react`; the workspace `devDependencies` `@workspace/eslint-config` and `@workspace/typescript-config` are removed (the lint preset is host tooling of Datyx). `tsconfig.json` is self-contained, with the values of the former preset. No export, type or behaviour changed.
- The package is registered as a product in `packages/repo-tools/src/checks/reusable-package-register.json` and can be transferred with `rps:transfer` and `rps-apply` (README § Use in another repository); no release is recorded.

### Fixed — a reverted Transaction withdraws invocations of a pending owner too

- **An invocation created inside a Transaction or a `ctx.commit` that reverts now ends `cancelled('transaction-reverted')` also when its Operation's Scope is pending** (ASFINAL-01). Previously such an invocation stayed `queued` behind the pending Scope, and activating that Scope later started it: its run did external work and its `ctx.commit` succeeded, although the Transaction that created it had reverted. For all three policies. A frame that commits still leaves the invocation waiting for the activation, and it starts exactly once then.

### Documentation — the `useScoped` grace before disposal

- The docs and TSDoc of `useScoped` no longer claim that nothing observes a commit made after unmount and before the disposal microtask (ASFINAL-02). During that grace React no longer renders the model, but the model's Scope is still `active` and its own Effects react to such a commit, side effects included. Only the disposal ends the model's authority; code that must not act after the component has gone decides on ownership and outcome, not on the absence of React subscribers. The mechanism is unchanged.

### Documentation — commands on a disposed `useScoped` model

- The docs and TSDoc of `useScoped` no longer claim that handlers which captured a disposed model are inert. A disposed model's Operations never regain commit authority and its subscriptions and Effects are released, but a command still invoked on it obeys the disposed-resource rules: writes are ignored and invocations end cancelled, while reading a model Derived that was never evaluated throws `DisposedResourceError` (R12). Consumers avoid invoking commands on an ended model. The behaviour is unchanged.

### Changed (breaking) — the independent engine replaces the previous one

The engine that `@workspace/active-state` ships was replaced as a whole by an independently designed one (`docs/ARCHITECTURE.md`, `docs/DECISIONS.md` Besluit 5). Exactly one engine ships; the previous engine, its tests and its React hook are deleted, and there is no compatibility layer. The package is internal and unpublished: "breaking" describes the in-repository API, not a public compatibility promise. The deliberate behaviour changes are C1–C14, ratified as R1 (`docs/DECISIONS.md` Besluit 5).

**New public concepts**

- `Scope`: explicit lifetime and deterministic disposal; pending Scopes build a model without starting anything.
- `Operation`: owned async work with a policy (`latest-wins`, `drop-while-running`, `queue`), `Invocation` handles, a status source, typed non-throwing outcomes (`succeeded`, `failed`, `cancelled`, `superseded`) and `ctx.commit`, the only protected way for a run to change state.
- `transaction(fn)`: synchronous, flat, all-or-nothing local state; a reverted Transaction publishes nothing.
- Error classes: `PurityViolation`, `StructuralMutationInTransaction`, `LifecycleViolation`, `DisposedResourceError`, `DependencyCycleError`, `CommitNestingError`, `CommitRevoked`, `FeedbackLimitExceeded`.
- Types: `Source`, `ValuesOf`, `EffectContext`, `ScopeOptions`, `ScopeState`, `OperationOptions`, `OperationContext`, `OperationStatus`, `Invocation`, `InvocationId`, `InvocationStatus`, `Outcome`, `CancelReason`, `Policy`, `NotThenable`, `GuardedCall`.
- `useScoped` in `./react`, **EXPERIMENTAL** (its exit gate passed and the export was approved, R14); see `docs/API.md` for the supported host and the factory obligations.
- A third entry point, `@workspace/active-state/testing`: strict error capture, event recorder, inspection, controlled Operation runs, and the internal, unstable instrumentation protocol.

**Changed**

- `Derived`: `{deps, fn: ({currDepVals}) => …}` → `{inputs, compute: (...values) => …}`. Construction computes nothing (C1). There is no `mount()`: a read is always current, and observation activates (C2). A Derived publishes when its settled value differs from its last published value (C3).
- Option `compare` → `equal` on Store and Derived (C9).
- Listeners receive `(value, previous)` instead of `{prevVal, currentVal}`; `prevState` is gone (C6). A compute receives only its input values (C7).
- `batch(fn)` groups publication only: a throwing callback keeps its writes, publishes them and rethrows (C8, E3). It returns the callback's value. `batch`'s `onError` option is removed.
- `Effect` is `new Effect({scope, inputs, run, immediate})`, started by its Scope, with a run Scope per run and a synchronous cleanup; `eager` and `mount()` are gone.
- Updaters, every equality and Derived computations are pure contexts: a mutation or lifecycle call from them throws `PurityViolation` (C13). Inside an open Transaction, structural and lifecycle calls are rejected (C14).
- Failures without a synchronous caller go to one error sink per turn (`reportError` by default) instead of a `queueMicrotask` throw (C12).
- Disposed resources are inert instead of absent (C11).
- `Store` has one type parameter and still has no default equality (R7 remains open).

**Removed** (R2): `ReadonlyStore` (use `store.asReadonly()`, typed `Source<T>`), `ReadonlyDerived`, `DerivedFnProps`, `UnwrapDerivedOrStore`, `StoreUpdateInput`, `StateChange`, `DependencyGraph`, `EffectCleanup`, `BatchOptions`, `debugGraph`, `debugGraphMermaid`; Store and Derived `onUpdate` and `onSubscribe` (C4, C5); the custom `updateFn` strategy and `TUpdater` (C5); `listenerCount`.

**Migration.** In-repository consumers were migrated in the same change: the auth provider (`compare: shallow` → `equal: shallow`) and the notification-batch wizard model (`inputs`/`compute`; the permanent `mount()` loop deleted) in `@workspace/active-auth-ui`. Per former export:

| Former | Now |
| --- | --- |
| `new Derived({deps, fn: ({currDepVals: [a, b]}) => …})` | `new Derived({inputs: [a, b], compute: (a, b) => …})` |
| `derived.mount()` / its release handle | Nothing: reads are always fresh; observation (`subscribe`, `useStore`, an input of an observed Derived, an Effect) activates |
| Store and Derived option `compare` | `equal` |
| Listener `({prevVal, currentVal})`; `Store.prevState`; `Derived.prevState` | Listener `(value, previous)`; no `prevState` |
| `prevVal`, `prevDepVals` in the compute | Removed: a Derived is a value. Memory belongs in a Store |
| Store `onUpdate`, `onSubscribe`; Derived `onUpdate`, `onSubscribe` | Removed. React to publication with `subscribe` or an Effect |
| Custom `updateFn` and the `TUpdater` parameter | Removed: `update((current) => next)` |
| `batch(fn, {onError})` with rollback on throw | `batch(fn)` without rollback; use `transaction(fn)` for all-or-nothing |
| `new Effect({deps, fn, eager})` + `effect.mount()` | `new Effect({scope, inputs, run, immediate})`, started by its Scope |
| `ReadonlyStore` class, `ReadonlyDerived` | `store.asReadonly()` typed `Source<T>`; a `Derived` has no write members to hide |
| `debugGraph()`, `debugGraphMermaid()`, `DependencyGraph` | The instrumentation stream and inspection in `/testing` |
| `StateChange`, `DerivedFnProps`, `UnwrapDerivedOrStore`, `StoreUpdateInput`, `EffectCleanup`, `BatchOptions` | Removed; `ValuesOf` types a Derived's inputs |
| Propagation errors thrown from `queueMicrotask` | The error sink (`reportError` by default), replaceable in tests |
| `listenerCount` | `inspectStore(store).subscriptions` / `observerCount(source)` in `/testing` |

The entries below this one, down to `[1.0.1]`, describe changes to the replaced engine that were made before it was replaced; none of them was released on its own.

### Removed (breaking) — standards convergence

- **`shallowCompare` removed.** It was a deprecated alias of `shallow` with no
  consumer. Migration: import `shallow`, which is the same function.

### Fixed — standards convergence

- **`debugGraph()` edges follow the runtime graph.** Edges were read from the
  caller's `deps` array, so mutating that array after construction made the
  debug edges disagree with the runtime graph and the node rows; they now come
  from each node's own dependency snapshot.

### Documentation — standards convergence

- `API.md` now states that `Store.prevState` inside a `batch` stays the last
  published value, that a Store `onUpdate` that already ran in an aborted
  frame is not rolled back, and that `shallow` counts an inherited array index
  as present.

**The correctness programme ratified on 2026-09-15 was completed on 2026-09-16** (`docs/DECISIONS.md`, Besluit 2; `docs/ROADMAP.md` §5). Correctness 01 repaired activation, graph teardown and the `useStore` selection cache; Correctness 02 replaced the propagation machinery; Correctness 03 made activation ownership, Effect lifetime and dependency configuration explicit; Correctness 04 closed the public React, equality, readable-input and type boundaries. No expected-failure witness remains. The independent final review of 2026-09-16 reopened that closure for bounded remediation (`docs/ROADMAP.md` §5.4). 05A, 05B and 05C below are complete and repair the review's seven P1 findings. That is remediation evidence, not an independent re-audit. The independent closure re-review of the same day found three remaining P1 edge paths (AS-RECHECK-001 to -003). 06A below repairs AS-RECHECK-001, 06B repairs AS-RECHECK-002 and 06C repairs AS-RECHECK-003 with a breaking change to the mutation API (`docs/DECISIONS.md`, Besluit 3). The final closure review of the same day found three new P1 edge paths (AS-FINAL2-001 to -003); 07A, 07B and 07C below repair them. The independent technical closure verification of 2026-09-16 then concluded **closed with P2/P3 debt** (P0 = 0, P1 = 0); the remaining P2/P3 debt stays deferred. No version was released and publication was not started.

### Fixed — Correctness remediation 07C (a custom strategy cannot be absent)

- **Explicit `undefined` no longer stands in for a custom strategy (AS-FINAL2-003).** Three constructions of a custom updater Store compiled without a cast and reached the runtime without a strategy: `StoreOptions<State, Patch>` with `updateFn: undefined`, an explicit `undefined` options argument, and typed options whose `updateFn` was set to `undefined` before construction. `update` then ran the default updater and could store a partial value as the whole state. The custom requirement was `{updateFn: unknown}` and `[options: unknown]`, which admit `undefined` without `exactOptionalPropertyTypes`.
- **Types.** In custom mode `updateFn` is now a required function and the options argument a required object, so all three constructions — and deleting `updateFn` — no longer compile. Inference from an annotated inner `updateFn` parameter, an `any` updater (inline stores in dependency lists), and an update input type that itself permits `undefined` are unchanged.
- **Runtime.** The constructor throws a `TypeError` when the options carry an `updateFn` key whose value is not a function, in every mode, so JavaScript callers and unsafe casts cannot configure an absent strategy either. This also refuses `{updateFn: undefined}` on a default Store, which still compiles there; omit the key instead. An omitted or `undefined` options argument carries no mode at runtime and is refused by the types alone.
- Unchanged: the strategy is captured at construction; default stores need no options; `setState`, `update`, variance and scheduling behave as before. No export changed.

### Fixed — Correctness remediation 07B (cleanup handoff between executions of one Effect lifetime)

- **A nested execution no longer loses its cleanup (AS-FINAL2-002, S0-D6, S0-D7).** An eager body that writes before returning publishes outside a running drain and runs the body again within the same lifetime. The nested execution installed its cleanup, and the outer execution then overwrote it with its own when it returned, so the nested cleanup never ran. This reproduced with `setState`, with `update` and through an upstream `Derived`.
- **The latest started execution owns the installed cleanup.** A cleanup returned by an execution that a newer execution of the same lifetime superseded now runs at once, exactly once, when that older execution returns. Every execution still runs the installed cleanup before it starts. A nested execution that throws still supersedes the outer one; an outer execution that throws after a nested success leaves the nested cleanup owned.
- Unchanged: lifetime identity and the 06A handoff rules, disposal, teardown order and error aggregation, and scheduling. A body running inside a drain does not reenter. No public API, export or type changed.

### Fixed — Correctness remediation 07A (comparator-equivalent publication parity)

- **A read no longer manufactures a publication that a comparator would suppress (AS-FINAL2-001, S0-D1, S0-D3).** When a `.state` read refreshed an active `Derived` with an intermediate value and a later wave produced a result equivalent under `compare` to the published value, but through different dependency values, the candidate was compared with the intermediate value, accepted, and published as `{0 → 0}`. The same run without the read published nothing. With an object output the extra publication reached child Deriveds.
- **What a comparator compares a candidate with.** While the cache holds a value that was never published, a new candidate is first compared with the value last published. If `compare` finds them equivalent, the derived takes its published value again, committed from the published dependency values, and nothing is published. Otherwise the candidate is compared with the cached value as before. A read still returns the current value.
- Unchanged: without `compare` a changed dependency value still publishes, including a constant output and an Effect; the no-read path makes the same comparisons as before; a throwing `compare` consumes and commits nothing; exact-input reverts (06B) behave as before. No public API, export or type changed.

### Changed — Correctness remediation 06C (explicit mutation intent) — BREAKING

- **`setState` stores its argument as given; updater intent moved to `update` (AS-RECHECK-003, Besluit 3).** `store.setState(next)` never interprets or calls `next`, whatever its type. Computing the next state from the current one is `store.update(...)`.

  ```ts
  // Before
  store.setState((prev) => next);
  // Now
  store.update((prev) => next);
  ```

  `store.setState(callable)` always stores `callable` itself. A function-valued state is an ordinary value (`handler.setState(nextHandler)`); the former recipe `setState(() => nextHandler)` is now `update(() => nextHandler)`. Previously a callable whose static type hid its callability, such as `const erased: {length: number} = () => 42`, compiled as a direct value and was run as an updater, storing `42`.
- **A declared custom updater requires its `updateFn` (AS-RECHECK-003).** `new Store<State, Patch>(initial)` and `const options: StoreOptions<State, Patch> = {}` no longer compile. Previously the store fell back to the default strategy and could store a partial value as the whole state. `update` on a custom store accepts exactly `TUpdater`; `setState` still replaces the state directly.
- **The update strategy is fixed at construction.** Removing or replacing `updateFn` on the caller's options object afterwards no longer changes how `update` works.
- **`Store` is invariant in `TUpdater`.** A custom store cannot be assigned to a default `Store<TState>` or to a store with another updater, and vice versa. `TUpdater` no longer has a constraint, so a custom update input may be any type, for example a `Partial<State>` patch.
- **A `Symbol.hasInstance` object is a valid direct value (AS-RECHECK-004, incidental).** The negative callability marker that rejected it is gone, together with the filtering of callable union members. A direct `setState(value)` in generic code over `T` now compiles.
- **Removed exports:** `Updater<T>`, `AnyUpdater` and `isUpdaterFunction`. They existed only for the combined value-or-updater `setState`. `StoreUpdateInput<TState, TUpdater>` remains and now describes the input of `update`.
- Unchanged: `compare`, acceptance, `onUpdate` timing, batching and rollback, listener publication, `prevState`, the scheduler, Derived, Effect and React behaviour. Both methods enter the same accepted-write path; a throwing updater or `updateFn` throws synchronously and commits nothing.
- Migrated in-repository callers: 18 package test calls, 3 in `@workspace/active-auth-ui` and 41 in `@workspace/active-grid`.

### Fixed — Correctness remediation 06B (read and revert publication parity)

- **A pure read no longer manufactures a publication after an input returns (AS-RECHECK-002, S0-D1, S0-D3).** When a `.state` read or `debugGraph()` refreshed an active `Derived` for a new input and the input then returned to the value it was last published for, settlement recomputed that input under a new identity. For an object-producing `fn` the new identity counted as a transition, so the Derived published, and its child Deriveds and Effects reacted, where the same run without the read published nothing. This also happened after a nested revert, after a failed frame followed by a write of the published value, and after a failed settlement followed by the input returning.
- **Clarified contract: a Derived is a value, not a reducer (S0-D15, S0-D16).** A Derived's result is semantically determined by its current dependency values. `prevVal` and `prevDepVals` keep their meaning and remain available as incremental-computation context, but a computation must produce the same result as a full computation from `currDepVals`; counting evaluations, accumulating events or making reads meaningful is not supported, and a `compare` must only suppress semantically equal candidates. Not enforced at runtime.
- **An active Derived returns to its published state for its published inputs.** For inputs equal to the ones behind its last publication, an active Derived takes its published value again instead of running `fn` once more: a Derived is determined by its current dependency values, so those inputs can only produce that value. `.state` then returns the published value itself, and `prevState` is the value the Derived held before. An inactive Derived and activation catch-up compute as before.
- Unchanged: publication still follows the committed value or the dependency values behind it, so constant outputs and Effect reactions still publish for changed inputs; a failed settlement publishes nothing; `compare` still decides every new candidate. No deep equality was added. No public API, export or type changed.

### Fixed — Correctness remediation 06A (Effect lifetime and resource handoff under reentrancy)

- **A cleanup that ends the lifetime prevents the next body (AS-RECHECK-001, S0-D7).** When the previous cleanup disposes the Effect or releases its last owner, the execution it preceded no longer starts, and no cleanup is left behind. Previously the next body ran anyway and its cleanup was retained on an inactive or disposed Effect.
- **An acquisition that outlives its lifetime is handed back (AS-RECHECK-001, S0-D4).** When a callback run by the dependency acquisition disposes the Effect, the acquired dependency is released at once, no setup runs and `mount()` returns an inert handle. A failure of that release is reported through the propagation channel. Previously the handle was installed after disposal, the eager body ran, and the upstream ownership survived every later release.
- **A teardown releases only the lifetime it ends (AS-RECHECK-001, S0-D4).** A cleanup that mounts the Effect again starts a new lifetime with its own dependency ownership, which the ending teardown no longer releases. Previously the teardown released the new lifetime's handle and lost the old one, leaving the internal dependency tracking active without an owner.
- **A captured publication belongs to the lifetime it was captured for (AS-RECHECK-001, S0-D6).** A lifetime that begins during a publication — for example a release and `mount()` from a Store listener, or a remount from a cleanup — does not run its body for a transition captured before it began; it reacts to later waves as usual. Previously an eager replacement ran a second setup right after its own.
- No public API, export or type changed. `Effect._isMounted` and `Effect._cleanup` (internal) are now derived from the current lifetime.

### Fixed — Correctness remediation 05C (setter type soundness, shallow symmetry)

- **The update strategy is decided by the declaration (AS-FINAL-006).** `TUpdater` on `Store`, `StoreOptions` and `StoreUpdateInput` now defaults to `never`, meaning "default strategy". A declared custom updater selects the custom strategy whatever its shape. Previously the strategy was inferred structurally, so `new Store<number, (n: number) => number>(0, {updateFn: (n) => (fn) => fn(n)})` still accepted `setState(42)`, which threw a `TypeError` at runtime.
- **A function is never a direct value (AS-FINAL-006).** The direct-value form of the default strategy removes callable union members and refuses a function for object members. Previously `new Store<(() => number) | null>(null).setState(() => 42)` compiled as a direct callable value, ran as an updater and stored `42`. To store a function, return it from an updater: `setState(() => nextFunction)`. `unknown` and `any` state are unchanged.
- **An undeclared custom updater is `unknown`.** An `updateFn` whose inner parameter is not annotated, on a store without an explicit `TUpdater`, now types that parameter as `unknown` instead of the default updater shape, so the strategy must declare what it accepts. **Breaking at the type level only**: such a declaration no longer compiles if it uses its input. The one in-repository case was a package test; no consumer used `updateFn`.
- **`shallow` is symmetric and does not read prototype getters (AS-FINAL-007).** An object-valued getter used to be skipped based on the left value only, so `shallow(a, b)` and `shallow(b, a)` could disagree, and the default equality of `useStore` could retain an obsolete selection. Same-prototype objects are now compared by all their own property keys (strings and symbols, enumerable or not). An instance that owns no property is opaque and unequal to another instance, and `URL` is a dedicated family compared by `href`. Behaviour changes: two distinct getter-only class instances, and two `URLSearchParams`, are no longer equal; a differing non-enumerable string key next to enumerable keys now makes objects unequal. Every change answers "not equal" where an equality was previously invented.
- Runtime of `Store.setState` is unchanged. No export was added or removed.

### Fixed — Correctness remediation 05B (exception-safe ownership, Effect terminality)

- **A throwing cleanup no longer strands ownership (AS-FINAL-003, S0-D4).** Releasing a `Derived` subscription removes the owner, runs its cleanup and deactivates the node when it was the last owner, whatever the cleanup throws. Previously the node stayed active without an owner and kept recomputing.
- **Every teardown step is attempted (AS-FINAL-003).** Deactivation attempts every upstream release even when an earlier one throws, so no upstream ownership is left behind.
- **Release errors are thrown after the release is complete.** An explicit release throws synchronously once every step it owns has run: one failure keeps its identity, several arrive as one `AggregateError`. This applies to `Derived` handles, `Effect` handles and `Effect.dispose()`.
- **A failed acquisition keeps its own error (AS-FINAL-003, S0-D10).** When activation fails and its unwind also throws, `mount()` or `subscribe()` throws the original activation error, and the unwind failures are reported through the propagation channel in one delivery. Previously an unwind failure replaced the original error.
- **Reentrant acquisition is consistent (AS-FINAL-003).** Activation is decided when an owner is committed, after its `onSubscribe` has returned, from the owners that exist at that moment. A subscription or mount made inside an `onSubscribe` that then throws stays live and active. An owner acquired while an activation of the same node is running joins it instead of recursing; if that activation fails, the joined owner is released with it and its handle is inert.
- **Disposal and release are terminal for captured work (AS-FINAL-004, S0-D7).** A wave captures its publication before callbacks run; an Effect disposed or released by an earlier callback of the same wave no longer runs its body.
- **A cleanup cannot outlive its lifetime (AS-FINAL-004, S0-D6).** When an Effect body disposes or releases its own Effect and returns a cleanup, that cleanup runs immediately and exactly once instead of being retained. A `mount()` whose eager setup disposed the Effect returns an inert handle.
- **One setup per activation under reentrancy (AS-FINAL-004).** A `mount()` inside the eager setup joins the running activation instead of starting a second setup.
- **Effect teardown order (AS-FINAL-004).** Deactivation runs the Effect's own cleanup first and then releases its dependency tracking — the reverse of acquisition — and attempts both even when one throws. Previously a failing upstream release skipped the Effect's own cleanup.
- No public API, export or type changed.

### Fixed — Correctness remediation 05A (Derived evaluation, purity, reads versus publication)

- **A failed comparison consumes nothing (AS-FINAL-001, S0-D11).** `Derived` used to record the consumed dependency values and the validated epoch before running `compare`, so a throwing comparator left the failed candidate looking settled: later reads and waves with the same dependency values silently returned the old output and never retried. One evaluation is now `fn` followed by `compare`, and nothing — consumed inputs, freshness, value, history — is committed until both succeed.
- **`compare` shares the purity boundary of `fn` (AS-FINAL-002, S0-D13).** A Store write from a Derived comparator is now diagnosed exactly like a write from `fn`, during a lazy read, a wave and activation catch-up alike.
- **Pure reads no longer change what a wave publishes (AS-FINAL-005, S0-D1, S0-D3).** A wave decided publication from whether its own settlement recomputed, so a `.state` read or `debugGraph()` before the drain could consume the transition: a same-primitive output and an Effect's internal `undefined` computation were not published, and the Effect did not run. Publication now compares the committed value and the dependency values that produced it with the last published baseline. A read before the drain can therefore neither suppress nor create a publication, and a node whose settlement fails in a wave publishes nothing, not even a value an earlier read refreshed.
- No public API, export or type changed. Waves, depth ordering, payload capture, the 100-wave budget and both error channels are unchanged.

### Documentation — current-contract cleanup (2026-09-16)

- **The package documentation was audited against the implementation** and corrected where it still described the pre-correctness model. The material corrections: neither `Store` nor `Derived` has a default equality comparator (a `Store` without `compare` accepts every write, an identical value included); rollback runs no Derived repair pass but invalidates the pure caches; cycle detection happens on activation, not on `mount()` alone; `Derived.listenerCount` is now documented; and `DerivedOptions.onSubscribe` was missing from the options table.
- **The README no longer presents the package as externally publishable or MIT licensed.** It is a private workspace package under the repository's proprietary `LICENSE`, with `UNLICENSED` in the manifest. The TanStack Store attribution is kept as a statement of origin without a legal conclusion; confirming the provenance and the notices it requires belongs to the deferred publication workstream. **No license file, manifest field, export or version was changed.**
- **`docs/PUBLISH-GUIDE.md` is marked historical and must not be executed.** Publication to GitHub or npm is not a current workstream (Walt, 2026-09-16); the roadmap now says so explicitly instead of presenting it as the next phase.
- React claims stay bounded to the evidence that exists: real `React.StrictMode` and one suspended transition, in jsdom. No browser, hydration or SSR acceptance is claimed anywhere.
- Documentation only. No runtime behaviour, no source, no test and no configuration was changed.

### Documentation — standards migration

- **The synchronous callback boundary is now stated.** `batch`, listeners and the `Effect` body are synchronous contracts. TypeScript structurally accepts an `async` function in those positions because a `Promise` satisfies a `void` return, but nothing awaits it: a write made in a later microtask falls outside the batch frame, and a rejection neither rolls back nor reaches `onError`. Behaviour is unchanged; this documents what was always true.
- Both entry points now enumerate their exports instead of re-exporting `*`. The exported symbols are identical — publication is deliberate rather than a side effect of what lives in `core` — and the duplicate `shallow` re-export on the React entry point is gone.

### Fixed — Correctness 04 (React selection, equality, readable inputs, types)

- **Selection cache ownership (AS-03, remaining).** A cached selection is reused only when it was computed for the same Store **and** judged by the same equality function, and returned unselected only when the selector and the snapshot are identical too. Previously a selection retained under a different Store or a different equality function could be returned as soon as the current equality function called it equal. The cache still survives a change of selector identity — that is what keeps inline selectors safe — so the guarantee is per evaluation: the value returned is one the current equality function calls equal to what the current selector produced for the current snapshot.
- **Equality boundary (AS-12).** `shallow` now compares arrays by length and by hole pattern (a hole is not a present `undefined`), requires a shared prototype before comparing keys, and answers "not equal" for instances whose state cannot be read — distinct `Promise`s, `WeakMap`s, classes with private fields. `Map`, `Set`, `Date`, plain objects and getter-bearing prototypes such as `URL` keep their behaviour. A getter that throws reaches the caller, by contract.
- **Readable inputs (AS-13).** `useStore` accepts `Store`, `ReadonlyStore`, `Derived`, `ReadonlyDerived` and anything else satisfying the new `ReadableStore<TState>`: current state plus a synchronous subscription with a reliable release. `ReadonlyDerived` is unchanged — narrowing its projection remains a separate breaking decision.
- **No-selector typing (AS-14).** Without a selector the hook returns the store's state type. `useStore<number, string>(numericStore)` no longer compiles, and the returned type can no longer be named by the caller.
- **Update forms follow the update strategy (AS-14).** `setState` accepts what the store's strategy can process, expressed by the new `StoreUpdateInput`: a store with a declared custom updater no longer accepts a direct value its `updateFn` would call as a function, and a function-valued state accepts only the updater form (`setState(() => nextFunction)`), because a function argument is always read as an updater.
- **Internal erasure stays internal (AS-15).** Where no dependency tuple is declared, `currDepVals` values are `unknown` instead of the `any` that variadic-tuple inference needs internally.

### Changed — Correctness 04

- `Store.setState` is one signature (`StoreUpdateInput<TState, TUpdater>`) instead of three overloads, and `useStore`'s two overloads accept `ReadableStore<TState>` instead of the concrete `Store` and `Derived` classes. Valid calls are unaffected; previously accepted unsound calls now fail typecheck.
- New exported types: `ReadableStore` and `StoreUpdateInput`.
- `DerivedFnProps.prevVal` keeps the type `unknown`. Typing it as the output type makes the inference circular — the output type is inferred from that same function's return value — and measured on TypeScript 6.0.3 every non-annotated `Derived` then inferred `unknown`. The docblock now carries that reason and the narrowing recipe.
- Documented React claims are narrowed to what is proved: real `React.StrictMode` and one suspended-transition render version in jsdom. No browser, hydration or SSR acceptance is claimed.

### Fixed — Correctness 03 (lifecycle)

- **Independent activation ownership (AS-09, S0-D4).** Every successful `mount()` and `subscribe()` creates its own owner; a Derived stays active while any owner is live. A handle releases only its own ownership, is idempotent, and a handle from an earlier lifetime no longer ends a newer one. Releasing a manual mount no longer deactivates a node that still has subscribers.
- **Failed acquisition leaves nothing behind (AS-09, S0-D4).** A throwing `onSubscribe` leaves no listener, and a failed activation releases the upstream Derived subscriptions it had already acquired — the orphaned upstream ownership that Correctness 02 had to park is gone.
- **Duplicate subscriptions are independent (S0-D5).** Subscribing the same listener function twice now creates two subscriptions: both are delivered, each owns its `onSubscribe` cleanup, and each is released separately. Listener-identity deduplication is no longer the subscription contract.
- **Effect construction is side-effect free (AS-10, S0-D6).** `eager` no longer runs the body in the constructor. It runs once on every transition from inactive to active, exactly one setup is live while active, the last release runs its cleanup, and a later activation runs setup again.
- **Effect cleanup runs at most once (AS-10).** The live cleanup is released before it is invoked, so a throwing cleanup is never invoked a second time. A throwing setup registers no cleanup and leaves the Effect inactive.
- **Effect return contract (S0-D8).** A body returning anything other than `undefined` or a cleanup function — a Promise included — is diagnosed at the setup boundary instead of being stored and later called.
- **`dispose()` is final (S0-D7).** It works in every state, including while active, is idempotent, runs the live cleanup at most once, makes handles handed out earlier inert, and diagnoses any later activation attempt.
- **Owned dependency configuration (AS-11).** `Derived` and `Effect` copy their dependency list at construction. Replacing or mutating the caller's options or array afterwards cannot change the live graph, and the caller's own array is no longer frozen on its behalf.

### Changed — Correctness 03

- `Store.listeners` (a `Set`) is replaced by internal subscription records; `Store.listenerCount` is unchanged and `Derived.listenerCount` is new. Both are the supported way to observe subscription counts.

### Fixed — Correctness 02 (propagation)

- **Settle before publishing (AS-24, S0-D1).** A wave first settles all affected pure Derived state; only then do Store listeners, `Derived.onUpdate`, Derived listeners and Effect bodies run, against that settled snapshot. A Store listener no longer observes a new Store value beside stale derived values. Effect bodies therefore run in the publication phase; their lifecycle is unchanged.
- **Reads are freshness-correct (S0-D2, S0-D3).** Reading `Derived.state` recomputes purely when a dependency changed, whether or not the derived is active. A read never mounts, subscribes, notifies, runs `onUpdate` or runs an Effect. This replaces the previous behavior where an inactive derived stayed stale until activation.
- **Accepted writes are owned before callbacks (AS-04, S0-D9).** The enclosing batch frame and the propagation queue take ownership before `onUpdate` runs. A throwing or reentrant `onUpdate` can no longer leave a value that escapes both rollback and publication, and it no longer vetoes an accepted write.
- **Failed computations stay retryable (AS-05, S0-D11).** Dependency values count as consumed only after a computation succeeds, so the same inputs are retried on the next demand. A failing node keeps its last successful value; unrelated nodes still settle and publish.
- **Wave and frame bookkeeping (AS-06).** Compare suppression and published baselines are wave-owned, and a failing frame releases only the queue entries it created, so a nested failure no longer discards unrelated queued work.
- **History (AS-07, S0-D15, S0-D16).** `prevVal` passed to a computation is the previous successful output, preserving `null`; `prevDepVals` is the dependency tuple last consumed successfully rather than each dependency's own previous value; a Store listener's `prevVal` is the last published value, so reentrant waves report `0→1` then `1→2`; `null`, `undefined`, `false` and `0` baselines are preserved by presence rather than by a nullish fallback.
- **Rollback (AS-08, S0-D12).** A failing batch frame restores each Store it wrote to the value **and history** it had at frame entry, publishes nothing, and leaves no cache refreshed from aborted values authoritative — including a chain refreshed by a read inside the failing frame. `onError` failures no longer replace the original error.
- **Error delivery (S0-D10).** Errors from the caller's own operation stay synchronous: updater, `updateFn`, Store `compare`, the `batch` callback and a failing read. Propagation and publication errors are reported after propagation completes, never dropped, and never interrupt remaining recoverable work. A single error keeps its identity; several are delivered as one `AggregateError`.
- **Pure computation cannot write (S0-D13).** A Store write from inside a Derived computation or comparator is diagnosed.
- **Bounded feedback (S0-D14).** Propagation fails diagnostically after 100 waves per outermost propagation, discards the runaway work and leaves the scheduler usable. A deep graph settles in one wave regardless of node count.

### Fixed — Correctness 01

- **Derived activation catch-up (AS-01).** `mount()`, `subscribe()` and therefore `useStore(derived)` now bring a Derived and its upstream Derived chain up to date with dependency writes made while it was inactive. Previously activation consumed the changed dependency values without recomputing, so the Derived stayed stale until the next write. Catch-up notifies no listener and runs no `onUpdate`. Correctness 02 then made every read freshness-correct, so activation is no longer the only way to bring a derived up to date.
- **Failed activation is unwound.** When activation throws (for example a failing catch-up computation, a failing Derived-dependency subscription or a detected cycle), the Derived is left inactive and can be activated again; `subscribe()` keeps no listener and runs the `onSubscribe` cleanup, and `mount()` leaves no permanent mount behind.
- **Graph teardown ownership (AS-02).** Releasing a Derived now removes only the store registrations it owns. Previously a Derived reached through several dependency paths (a diamond, a duplicated dependency, a direct plus indirect path) could remove another live Derived's registration, which then silently stopped reacting.
- **`useStore` selection context (AS-03, runtime).** The cached selection is reused only for the same Store, selector and equality function. Previously a changed selector, a changed equality function or a replaced Store that shared the state reference returned the selection computed for the previous context.

### Changed (internal)

- `Derived.unregisterFromGraph()` takes no argument and follows the recorded store registrations. `useStore` uses a module-level default selector, so omitting the selector keeps a stable selection context.
- The scheduler owns three internal scopes — accepted write, batch frame and wave — plus a graph epoch that a `Derived` validates its value against. `Derived.state` and `Derived.prevState` are getters; `lastSeenDepValues`, `getDepVals`, `_sampleDepVals` and `checkIfRecalculationNeededDeeply` are replaced by `_ensureFresh`, `_readDepVals` and the consumed-dependency record. All are `@internal` members; the public API is unchanged.

### Tests

- 17 regression tests for AS-01, AS-02 and AS-03 (`activation-catch-up.test.ts`, `graph-teardown.test.ts`, `react/__tests__/use-store-selection.test.tsx`).
- 32 tests for the propagation contract in `propagation.test.ts`: AS-04 to AS-08, AS-24, inactive read freshness, the nested-frame rollback of a refreshed chain, pure-computation write rejection and bounded feedback.
- 7 expected-failure witnesses (`it.fails`) remain in `known-defects.test.ts` for AS-09, AS-10 and AS-11 (Correctness 03), each named with its finding ID and `active-state ROADMAP §5`.
- Four existing tests were updated to the ratified contract: inactive reads are fresh (three suites) and multiple propagation errors arrive as one `AggregateError`.
- Correctness 04 adds four suites: `shallow-boundary.test.ts`, `type-contracts.test.ts`, `react/__tests__/use-store-readable.test.tsx` and `react/__tests__/use-store-selection-ownership.test.tsx`. The type witnesses are compile-time (`@ts-expect-error` plus annotated positive cases) and are checked by `tsc --noEmit`.
- Suite after Correctness 04: 23 files, 358 passed, no expected failures.

---

## \[1.0.1\] — 2026-05-20

**Internal type-cleanup.** Reduces `@typescript-eslint/no-explicit-any` warnings from 38 → 0 across the package via two centralized type-aliases and a handful of strict-replacements. No API surface changes; no runtime behaviour changes.

### Changed (internal)

- **New internal `AnyDep` type-alias** in `core/derived.ts` for the `Derived<any> | Store<any>` pattern that appears in variadic-tuple-inference positions throughout `Derived` and `Effect`. Single `eslint-disable` on the alias-definition with documented bivariance-rationale (variadic-tuple-inference preserves per-element state-type across dependency-arrays — TS issue #28259-related). 28 scattered `any`-occurrences in `core/derived.ts` and `core/effect.ts` consolidated.
- **`AnyUpdater` (existing)** received its missing motivated `eslint-disable` directive in `core/types.ts`. The JSDoc already documented the intentional bivariance under `strictFunctionTypes`; the disable suppresses the lint warning to match.
- **`react/use-store.ts`** signatures rewired to use central aliases instead of inline `any`:

  - `Store<TState, any>` → `Store<TState, AnyUpdater>` (uses centralized updater-alias).
  - `Derived<TState, any>` → `Derived<TState>` (relies on TArr default).
- **`core/debug.ts`** `RegistryEntry<any>` → `RegistryEntry<object>` for the `FinalizationRegistry` generic — matches the underlying constraint (`T extends object`).
- **`core/derived.ts` `_detectCircularDeps`** internal helper uses `Derived<unknown>` instead of `Derived<any>` for path/visited tracking (5 occurrences — the function only reads `instanceof` so the state-type is irrelevant).

### Not changed

- Public API surface: no changes to `Store`, `Derived`, `Effect`, `useStore`, or any exported function/class shape.
- Runtime behaviour: no changes.
- Public type re-exports via `core/index.ts`: unchanged. `AnyDep` is intentionally **not** re-exported (marked `@internal`, parallel to its private-use rationale).

### Tests

- All 252 tests remain green (no behaviour changes, no test additions).

---

## \[1.0.0\] — 2026-05-19

**First stable major release.** Promotes the 0.2.12 surface to a stable
baseline with an API-stability promise from this version onward.

**Correction (2026-09-16):** this entry originally called that baseline
"externally-publishable". It was not, and nothing about the artifact changed
to make it so: the package was and is `private: true`, `UNLICENSED`, exported
as TypeScript source with no build. Read the promotion as an internal
milestone. The stability promise also did not survive the correctness
programme: Correctness 01 through 04 deliberately changed observable behaviour
under v1.x — see `[Unreleased]`.

No API changes versus 0.2.12 — this release is the version-promotion itself.
`shallowCompare` remains exported under the v0.2.3 `@deprecated` marker and
is scheduled for removal in v2.0 (see `API.md`).

### Notes for consumers

- Import paths, surface, and runtime behaviour are unchanged from 0.2.12.
- ~~The README license block has been corrected to match the `LICENSE` file
  (MIT — TanStack-Store-derivation). The previous "proprietary, all rights
  reserved" wording was misleading documentation, not a license change.~~
  **Correction (2026-09-16):** that entry was wrong in both directions. The
  `LICENSE` file is proprietary ("all rights reserved") and `package.json`
  declares `UNLICENSED` and `private: true`, so the "proprietary" wording it
  replaced was the accurate one. The MIT claim has been removed from the
  README and the architecture document. No license file or manifest field was
  changed then or now, and no redistribution right was ever granted.
- Full package documentation now lives under `docs/`; this `CHANGELOG.md`
  and `DECISIONS.md` moved from the package root into `docs/` as part of
  the same restructure.

---

## \[0.2.12\] - 2026-04-06

### Fixed

- **registerOnGraph sort unstable for mixed-depth diamonds** (STATE-BUG-11): Replaced direct-dep membership comparator with `__getDepth()`-based sort, consistent with `__flushAll()` topological ordering. Eliminates non-deterministic ordering for nodes without a direct dep relationship.
- **\_detectCircularDeps O(n²) path lookup** (STATE-ARCH-02): Replaced `Array.includes()` with `Set.has()` for O(1) cycle detection. Path array mutated in-place with push/pop instead of spread copy per recursion.

### Documentation

- **queueMicrotask error boundary limitation** (STATE-BUG-10): Expanded "Known Behavior" section in README with detailed explanation, try/catch code example for critical listeners, and global error handler tip.

### Tests

- registerOnGraph mixed-depth diamond: 2 tests — verifies `__storeToDerived` is depth-sorted after mount, and computed values are correct through a mixed-depth diamond graph.
- Total test count: 252 (up from 250)

## \[0.2.11\] - 2026-04-06

### Fixed

- **registerOnGraph() O(n²) sort** (STATE-PERF-01): Replaced `Array.includes()` with `Set.has()` in the dependency sort comparator. Sort is now O(n log n) instead of O(n² log n) for large fan-outs.
- **`prevVal: unknown | undefined` redundancy** (TYPE-02/QW8): Simplified to `prevVal: unknown` — `unknown` already includes `undefined`.
- **`AnyUpdater` any documentation** (STATE-TYPE-01): Added JSDoc explaining why `any` is intentionally required for bivariant function type constraint under `strictFunctionTypes`.
- **tsconfig extends wrong base** (STATE-TYPE-02): Changed from `nodejs.json` to `react-library.json` — package contains React bindings (`useStore`, `useSelector`).
- **debugGraphMermaid duplicate node IDs** (STATE-ARCH-01): Node IDs now use the unique `__identifiers` WeakMap (`store_0`, `derived_1`) instead of labels, preventing Mermaid from merging nodes that share a label.

## \[0.2.10\] - 2026-04-06

### Tests

- STATE-TEST-02: 3 tests — batch() with async operations: synchronous state flushed immediately, async setState runs outside batch scope, synchronous errors still trigger rollback.
- STATE-TEST-05: 4 tests — shallow() with URL objects: identical URLs, different URLs, URLs with paths/query params, URLs differing only in query param.
- Total test count: 250 (up from 243)

## \[0.2.9\] - 2026-04-06

### Fixed

- **Listener error re-throw deferred via queueMicrotask** (STATE-BUG-09): Listener errors collected during flush are now re-thrown via `queueMicrotask()` instead of synchronously. In React Concurrent Mode, the synchronous `throw errors[0]` propagated through React's commit stack, causing double error reporting (console + error boundary). The deferred throw keeps the failure off React's commit stack. **Correction (2026-09-16):** it does not reach a React Error Boundary either — a throw from a microtask surfaces in the global error handler (`window.onerror`), as `ARCHITECTURE.md` §6.3 states.
- **Effect eager cleanup leak** (STATE-BUG-03): Added `dispose()` method to `Effect`. When `eager: true`, the constructor runs `fn()` and stores its cleanup. If `mount()` is never called, the cleanup was never invoked — resource leak. `dispose()` runs the pending cleanup without requiring mount. No-op when already mounted.

### Tests

- STATE-BUG-09: 3 new tests — deferred error via microtask, subsequent updates not blocked, store + derived listener errors both deferred.
- STATE-BUG-05 tests updated: now verify errors are deferred (not thrown synchronously) while all listeners are still notified.
- STATE-BUG-03: 4 new tests — dispose runs eager cleanup, idempotent dispose, no-op when mounted, no-op when not eager.
- Total test count: 243 (up from 236)

## \[0.2.8\] - 2026-04-06

### Fixed

- **Rollback Derived recompute ordering** (STATE-BUG-08): After `batch()` rollback, affected Derived instances are now deduplicated and sorted by dependency depth (topological order) before recomputing. Previously, per-store iteration could recompute a child Derived (D2) before its parent (D1), causing D2 to see stale parent values combined with rolled-back store values.

### Tests

- STATE-TEST-NEW-01: 3 tests for chained Derived rollback with mixed store dependencies (S1→D1→D2, S2→D2) — verifies D2 is consistent after rollback, including deep chains (D1→D2→D3).
- STATE-TEST-NEW-02: 4 tests for nested batch rollback with Derived depending on stores from both inner and outer batch scopes — inner rollback, outer rollback, and eager recompute scenarios.
- Total test count: 236 (up from 225)

## \[0.2.7\] - 2026-04-05

### Fixed

- **Batch rollback resets Derived** (STATE-BUG-07): When `batch()` rolls back store states on error, all Derived instances that depend on those stores are now recomputed with the rolled-back values. Previously, a Derived that was eagerly recomputed mid-batch would retain the aborted batch values after rollback.

### Tests

- Batch rollback Derived sync: 2 tests verifying single and chained Derived values are reset after batch error + mid-batch recomputation.

## \[0.2.6\] - 2026-04-05

### Fixed

- **Deps immutability enforcement** (STATE-BUG-04): `Derived.options.deps` is now `Object.freeze()`'d in the constructor. Mutating deps after construction would corrupt the scheduler's topological sort depth cache. The depth cache is also cleared on unmount as defense in depth.
- **Listener throw resilience** (STATE-BUG-05): `__notifyListeners` and `__notifyDerivedListeners` now wrap each listener call in try/catch. A throwing listener no longer prevents subsequent listeners or stores from being notified. The first error is re-thrown after the flush completes.
- **Batch flush clarification** (STATE-BUG-06): Extracted `__flushAll()` as a dedicated entry point for flushing all pending updates. `batch()` now calls `__flushAll()` directly instead of passing an arbitrary store to `__flush()`. `__flush(store)` remains the entry point for `Store.setState()` (enqueue + flush).

### Tests

- Deps freeze test: verifies `push()` on frozen deps throws
- Listener throw resilience: 4 tests — store listener throw, derived listener throw, mixed store + derived throw, batch with throwing listener
- Batch flush: verifies all stores are flushed without arbitrary selection
- Total test count: 225 (up from 205)

## \[0.2.5\] - 2026-04-05

### Fixed

- **Scheduler: stale values in multi-store batch** (STATE-BUG-01): In convergent graphs (`Store1→A→D, Store2→B→D`) where both stores change in a batch, D was computed with `new A + old B` because stores were processed sequentially. Replaced per-store sequential processing with global topological flush: collect all affected deriveds, sort by depth, recompute each once, notify each once.
- **Scheduler: duplicate listener notifications** (STATE-BUG-01b): Deriveds registered at multiple root stores had their listeners called once per store instead of once total. Fixed by deduplication via Set collection before notification.
- **Compare suppression + duplicate notifications** (STATE-BUG-02): Compare-suppressed mid-chain deriveds still caused duplicate notifications on downstream deriveds in multi-store scenarios. Fixed by the same global flush approach.

### Removed

- `__flush_internals()` — replaced by linear topological processing
- `__depsThatHaveWrittenThisTick` — no longer needed with single-pass recomputation

### Tests

- 9 new tests for downstream derived listener notification: multi-store convergent graphs, compare suppression with alternate paths, mixed direct + indirect deps on same store, performance sanity check (20 stores, 30 deriveds, 3 levels)
- Total test count: 219

## \[0.2.4\] - 2026-04-04

### Breaking Changes

- Minimum React version raised to 19.0.0
- `use-sync-external-store` shim dependency removed — React 19's native `useSyncExternalStore` is now used

### Changed

- `useStore` hook now uses React 19's native `useSyncExternalStore` with custom selector caching

## \[0.2.3\] - 2026-04-04

### Changed

- `__flush` optimization: replaced `Array.from()` with Set swap pattern — eliminates array allocation per flush cycle
- `shallowCompare` marked as `@deprecated` — use `shallow()` as the canonical export

### Documentation

- Effect auto-mount design choice documented in README (why `mount()` is required)
- Framework-agnostic usage examples added to README (Node.js, Vanilla JS)
- README examples updated to use `shallow()` instead of `shallowCompare`

## \[0.2.2\] - 2026-04-04

### Fixed

- **useStore infinite loop** — Replaced `useMemo`-closure pattern with `useSyncExternalStoreWithSelector` from `use-sync-external-store` — inline selectors (new reference every render) no longer cause infinite re-renders
- **Derived chained auto-mount memory leak** — Derived instances now subscribe to their Derived dependencies during mount, triggering the dependency's auto-mount lifecycle — previously, `registerOnGraph()` directly registered dependencies on the graph without lifecycle management, causing them to persist and recompute after the parent unmounted

### Changed

- `useStore` now uses `useSyncExternalStoreWithSelector` internally — handles unstable selectors, custom equality, and server snapshots correctly out of the box
- `Derived._performMount()` now subscribes to Derived dependencies with a no-op listener to trigger their auto-mount — unsubscribe during `_performUnmount()` triggers their auto-unmount
- `Derived.registerOnGraph()` no longer directly calls `dep.registerOnGraph()` for Derived dependencies — they register themselves via their own auto-mount lifecycle
- Added `use-sync-external-store` as runtime dependency (~2KB, no transitive dependencies)

### Tests

- Inline selector stability (no infinite loop, correct updates, undefined handling)
- Chained derived unmount lifecycle (2-level, 3-level, shared dependency)
- Scheduler WeakMap cleanup verification after full unmount chain
- Effect with Derived dependency auto-mount/unmount
- Total test count: 205

## \[0.2.1\] - 2026-04-04

### Fixed

- **React Concurrent Mode safety** — `useStore` rewritten to use `useSyncExternalStore` with `useMemo`-scoped closure. Eliminates ref mutations during the render phase that could cause tearing under concurrent features (Suspense, `startTransition`). Follows the same pattern as Zustand v5.
- **Nested batch rollback — shared stores** — When a nested batch fails and one of its stores was already pending from an outer batch, the store is now correctly restored to the state it had when the nested batch started (not the original pre-outer-batch value). Previously, shared stores were fully rolled back, discarding valid outer-batch mutations.

### Added

- **Derived auto-mount** — `Derived` now automatically mounts on first subscriber (0→1) and unmounts on last subscriber leave (1→0). Manual `mount()` still works and acts as a "permanent" mount that disables auto-unmount. `useStore(derived)` no longer requires a prior `mount()` call.
- **FinalizationRegistry for debug cleanup** — Debug registry now uses `FinalizationRegistry` (when available) for automatic WeakRef cleanup on GC, instead of relying solely on manual `pruneRefs()` calls.
- **readonly state/prevState** — `Store.state`, `Store.prevState`, `Derived.state`, and `Derived.prevState` are now `readonly` at the type level. Internal mutations use a `Mutable<T>` cast pattern, preventing accidental external writes.

### Changed

- `shallow()` moved from `src/react/` to `src/core/` (backward-compatible re-export from react layer)
- `useStore` internals: replaced `useRef` + `useCallback` + `useSyncExternalStore` with `useMemo`-scoped closure + `useSyncExternalStore` for concurrent safety
- `Derived._isMounted`, `_manuallyMounted`, `_performMount()`, `_performUnmount()` added as internal lifecycle methods

### Tests

- React: StrictMode double-render, rapid sequential setState, selector reference stability, undefined selector values
- React: Derived auto-mount without manual `mount()`, auto-unmount on component unmount
- Scheduler: shared store rollback to outer batch value, mixed stores in nested batch, outer batch continues after inner rollback, double-nested rollback
- Derived: auto-mount on subscribe, auto-unmount on unsubscribe, manual mount prevents auto-unmount, chained derived auto-mount, re-auto-mount after cleanup
- Total test count increased from 181 to 197

## \[0.2.0\] - 2026-04-04

### Fixed

- `shallow()` now correctly handles getter-only objects (Temporal, DOM elements, Proxies) — previously returned false positives for objects with zero enumerable keys but with getters
- `batch()` now rolls back all state changes when an error occurs instead of flushing partial updates — nested batch rollback is scoped to the failing batch only

### Added

- `compare` option on `StoreOptions` and `DerivedOptions` — custom equality function to skip updates when state is semantically equal, preventing unnecessary downstream recomputes
- `shallowCompare` helper export for convenient use with `compare` option
- `ReadonlyStore` runtime wrapper — exposes `state`, `prevState`, `subscribe`, `listenerCount` without `setState`. Create via `new ReadonlyStore(store)` or `store.asReadonly()`
- `ReadonlyDerived` type export — Pick type for exposing derived values without internal methods
- Circular dependency detection in `Derived` — throws descriptive error with full cycle path at `mount()` time
- `label` option on `Store`, `Derived`, and `Effect` — debug identifier for use with debug helpers
- `debugGraph()` and `debugGraphMermaid()` — inspect the live dependency graph in development (no-op in production)
- `listenerCount` getter on `Store` — public alternative to internal `listeners.size`
- `batch()` now accepts optional `{ onError }` callback for error logging/telemetry
- `BatchOptions` type export

### Changed

- Moved `shallow()` from react layer to core — eliminates architectural dependency of core on react directory
- Scheduler internals: `Array.includes()` replaced with `Set.has()` for O(1) dedup in dependency tracking
- `Store.listeners`, `Derived` internal properties marked `readonly` / `@internal` for better encapsulation
- `Derived.recompute()` now returns `boolean` indicating whether the value actually changed

### Tests

- Diamond dependency patterns (classic, asymmetric, wide, with compare, with batch)
- Edge cases: `undefined` initial state, deeply nested derived chains (3–20 levels), subscribe/unsubscribe memory leak check, concurrent mount/unmount
- Scale tests: 100 stores + 50 derived batch update, 1 store → 50 derived fan-out, 20-level chain
- `ReadonlyStore` wrapper and `store.asReadonly()` caching
- Debug helpers: graph structure, labels, mount tracking, production no-op, Mermaid syntax
- Total test count increased from 103 to 181

## \[0.1.0\] - Initial release

- `Store`, `Derived`, `Effect` core primitives
- `useStore` React hook with selector and custom equality support
- `batch()` for grouped updates
- `shallow()` equality helper
