# Reactive-form

## Concept

A reactive-form expresses computation as a graph of derived values
where each node is a pure expression over upstream signals. Inputs are
the leaves; intermediate values and outputs are nodes that re-evaluate
when their upstream signals change. The framework runs the dependency
graph; the operator declares relationships, not control flow.

## Shape

Every reactive-form implementation carries these invariants:

- **Signals as the unit of state.** Each piece of state lives behind a
  named signal (observable, reactive cell, accessor). Reading a signal
  inside an expression registers the expression as a dependent;
  writing a signal triggers re-evaluation of dependents.
- **Derived values as pure expressions of signals.** Each intermediate
  computation is a function of upstream signals only — no reads from
  module-level state, no reads from imperative variables outside the
  reactive scope. The §Locality property from `code-design.md` applies
  per node: every input is a declared upstream.
- **Dependency tracking is automatic, not declared.** The runtime
  observes which signals an expression reads during evaluation and
  builds the dependency edge; the operator does not enumerate
  dependencies manually in a `[a, b, c]` array (or if the framework
  requires that, the array is generated from the expression body, not
  hand-maintained).
- **Updates propagate in topological order.** When a signal changes,
  the runtime re-evaluates dependents in dependency order, ensuring
  each derived value sees consistent upstream values. No mid-update
  observation of inconsistent intermediate state.
- **Effects are at the leaves of the graph.** Side-effecting work
  (DOM writes, network calls, file writes) lives in effect nodes
  (`effect()`, `createEffect`, `useEffect`, `autorun`) that subscribe
  to signals and perform IO when dependencies change. The decision
  layer above stays pure.
- **Glitch-free convergence.** A change to a single signal produces
  one re-evaluation per dependent, not N intermediate re-evaluations.
  The runtime batches updates and reads the final value once. This
  is what distinguishes reactive-form from naive observer chains.

## Forecloses

When a function is designed as a reactive-form, the following designs
are explicitly *not* the right choice for the same problem:

- **Manual subscribe / update / unsubscribe.** A function that
  manually calls `target.on("change", handler)` and bookkeeps its own
  subscriptions is not reactive-form — it is event-driven code. The
  framework owning the dependency graph is the defining feature;
  without it, the operator carries the bookkeeping burden.
- **Imperative state updates with re-renders triggered explicitly.**
  Calling `update_view()` after every state mutation is not
  reactive-form — it is push-based rendering. Reactive-form lets the
  runtime decide when to re-evaluate; explicit re-render calls
  defeat the abstraction.
- **Polling.** A `setInterval` that re-evaluates downstream every
  N ms regardless of whether upstream changed is not reactive-form —
  it is polling. Reactive-form's update semantics are
  change-triggered, not time-triggered.
- **Pipeline.** A pipeline (per pattern pipeline.md) runs end-to-end
  on each invocation; a reactive-form re-evaluates only the affected
  subgraph. Choosing reactive-form forecloses the
  every-step-runs-every-call pipeline shape — the change-driven
  semantics is structurally different.
- **Effects-as-data at the producer layer.** Effects-as-data
  produces a description that a separate runtime executes; the
  description has no upstream-signal dependencies it tracks
  automatically. A reactive-form's effect nodes run *because* an
  upstream signal changed — a different triggering mechanism than
  effects-as-data's interpret-a-plan mechanism.
- **Stateful class with observer methods.** A class that holds
  state in `self` fields and dispatches change notifications via
  manually-coded observer methods is not reactive-form — it is
  the GoF Observer pattern. Reactive-form's framework-owned graph
  is what makes the dependency tracking automatic; manual observer
  code does the opposite.

## Judgment probes

These checks verify that a function actually implements the
reactive-form pattern:

- **Signal-typed inputs and outputs.** Are the function's input
  state and output state expressed as signals (or reactive
  primitives equivalent in the framework), not plain values? If
  the function takes a plain value and writes to a plain value
  with no signal involvement, it is not participating in the
  reactive graph.
- **Pure derived expressions.** For each derived value (computed
  signal, memo, derived store), is its expression body a pure
  function of upstream signals — no reads from non-reactive state,
  no side effects in the derivation? If a derived value reads from
  module-level mutable state, dependency tracking is incomplete and
  staleness will result.
- **Automatic dependency tracking.** Does the framework infer
  dependencies by observing reads during evaluation — or does the
  operator hand-list dependencies? Hand-listed dependencies are
  brittle (forget one and you have a staleness bug); automatic
  tracking is the load-bearing feature.
- **Effects subscribe, do not poll.** Locate the side-effecting
  nodes (DOM writes, fetches, file writes). Do they trigger
  re-execution when upstream signals change — or do they run on a
  fixed schedule? If they run on a schedule, the pattern's
  change-driven semantics is bypassed.
- **No manual `update_view()` / `re_render()` calls.** Search the
  code for explicit invocations of re-render or update primitives.
  If they exist, the framework is not owning the propagation —
  the operator is.
- **Glitch-free under concurrent changes.** Update two upstream
  signals in the same tick and observe whether the downstream
  derived values re-evaluate once with both new values, or twice
  with intermediate inconsistency. If twice, the runtime does not
  batch — the implementation may technically be observer-based but
  lacks the convergence guarantee that distinguishes the pattern.
- **Effect placement audit.** Effects belong at the leaves
  (subscribing to signals and performing IO). Effects placed
  inside derived value expressions break purity and produce
  re-execution surprises. Walk the derived nodes; flag any that
  perform IO.

## Render: Python

**Primary applicability: UI code, not pure-Python server code.** The
project's reactive-form usage lives in `mcp-server/ui/src/` (SolidJS via
`createSignal` / `createMemo` / `createEffect`); see the SolidJS render
in the catalog for the canonical shape. Pure-Python code in this
codebase is request-driven, not signal-driven — there is no event loop
re-evaluating a dependency graph in the MCP server's hot path.

Where reactive-form does appear in Python, the idiomatic vehicles are:

- **RxPY** (`Observable` / `Subject`) for production signal graphs.
- **`async` generator pipelines** (`async for value in upstream():
  yield transform(value)`) for streaming derivation where the framework
  is the asyncio event loop.

A minimal hand-rolled Signal/Memo for testing / documentation purposes:

```python
from collections.abc import Callable
from typing import Generic, TypeVar

T = TypeVar("T")

class _Tracker:
    """Module-internal: the active observer being recorded."""
    current: "Memo | None" = None

_tracker = _Tracker()

class Signal(Generic[T]):
    """Read/write reactive cell. Reads register the active observer;
    writes notify all observers and trigger re-evaluation."""

    def __init__(self, value: T) -> None:
        self._value: T = value
        self._observers: set["Memo"] = set()

    def get(self) -> T:
        if _tracker.current is not None:
            self._observers.add(_tracker.current)
            _tracker.current._upstreams.add(self)
        return self._value

    def set(self, value: T) -> None:
        if value == self._value:
            return  # glitch-free: no notification on no-op write
        self._value = value
        for obs in list(self._observers):
            obs._reevaluate()

class Memo(Generic[T]):
    """Derived value. Pure expression of upstream signals; the framework
    tracks dependencies automatically by observing reads during eval."""

    def __init__(self, fn: Callable[[], T]) -> None:
        self._fn = fn
        self._upstreams: set[Signal] = set()
        self._reevaluate()

    def _reevaluate(self) -> None:
        prev, _tracker.current = _tracker.current, self
        try:
            self._value = self._fn()
        finally:
            _tracker.current = prev

    def get(self) -> T:
        return self._value

# Example: derived values re-evaluate when upstream signals change.
first = Signal("Ada")
last = Signal("Lovelace")
full = Memo(lambda: f"{first.get()} {last.get()}")
assert full.get() == "Ada Lovelace"
first.set("Augusta")
assert full.get() == "Augusta Lovelace"
```

Note the constraints inherited from `code-design.md`: the Memo's `fn`
must be pure (Rule 10 — no IO inside the derivation), and effect work
(file writes, HTTP) belongs in a separate `effect()`-style subscriber
that lives at the leaf, not in a Memo's expression body.

### Probes

- **signal-typed boundaries.** For a function tagged `@design pattern
  reactive-form`, its inputs and outputs must be `Signal[T]` or `Memo[T]`
  (or framework equivalents — `Observable`, `BehaviorSubject`). AST:
  walk `FunctionDef.args.args[*].annotation` and `FunctionDef.returns`.
  A function taking and returning plain values is not participating in
  the reactive graph.
- **pure derived expressions.** For each `Memo(lambda: ...)` /
  `createMemo(...)` construction, walk the lambda / function body for
  effect calls (`httpx`, `requests`, `subprocess`, `open`, `Path.write`,
  `print`). Any IO in a derivation breaks purity (Forecloses §Effects
  inside derived value expressions).
- **no manual `update_view()` / `re_render()` calls.** AST: search the
  module for calls to named re-render primitives. The reactive-form
  contract is that the runtime owns propagation; explicit
  re-evaluation calls violate the framework-owned-graph invariant.
- **no `setInterval` / `asyncio.sleep` polling loops.** AST: detect
  `while True:` bodies that contain `await asyncio.sleep(...)` followed
  by a read of an upstream signal. Polling violates change-triggered
  semantics (Forecloses §Polling).
- **dependency tracking is implicit, not hand-listed.** AST: for each
  Memo / observable construction that takes a `deps=[...]` array
  argument, verify the array is either absent (automatic tracking) or
  generated from the body (e.g., `createMemo(fn, deps_from_ast(fn))`).
  Hand-maintained dep arrays are brittle (Judgment probe §Automatic
  dependency tracking) — flag as a candidate review.
- **effects subscribe at leaves.** For each `createEffect(...)` /
  `Observable.subscribe(...)` site, verify it sits at the end of the
  reactive graph (no Memo / Signal reads downstream of it). AST: walk
  the module for nodes downstream of the effect's subscription chain;
  flag any Memo whose construction comes after the effect call.

### Example

`mcp-server/ui/src/` — the SolidJS components use `createSignal` /
`createMemo` / `createEffect` to wire the daemon's `/state` endpoint
into the UI. No pure-Python module in `forge_manager_mcp/` currently
implements this pattern; the daemon's watch loop in `daemon/` uses
async polling, not signal subscription, because the underlying
filesystem events are not naturally reactive without a watcher library
(out of scope per the daemon design decisions). If a pure-Python
reactive-form module appears in the future (e.g., a derived-state cache
for `gates_status_v0`), the rendering above is the forward-going shape.

### Anti-patterns

- **Class with `update()` method called from every setter.** A class
  holding state in `self` fields plus an `update()` method that
  recomputes downstream values on every setter call is push-based
  rendering, not reactive-form. The operator does the propagation; the
  abstraction is missing (Forecloses §Imperative state updates with
  re-renders).
- **`while True: await asyncio.sleep(0.1); recompute()` loop.** Polling
  the upstream state on a timer instead of subscribing to changes.
  Violates Forecloses §Polling; change-triggered semantics is bypassed.
- **GoF Observer with hand-coded `subscribers: list[Callable]` and
  `notify_all()`.** The dependency graph is hand-maintained — operator
  bookkeeps subscriber lists, forgets to unsubscribe, leaks memory.
  Violates Forecloses §Stateful class with observer methods (the
  framework-owned graph is the load-bearing feature).

## Render: Go

Go has no native reactivity primitive (no Solid/MobX-style automatic
signal tracking, no built-in computed-with-dependency-inference). The
reactive-form pattern in Go is rendered via one of three vehicles:

1. **Channel + goroutine graph** — the most native form. Each signal
   is a channel; each derived value is a goroutine reading from
   upstream channels and writing to a downstream channel. Dependency
   tracking is *not* automatic — the operator wires channels
   explicitly — so this form satisfies the change-propagation
   invariant but fails the automatic-dependency-tracking probe.
2. **Third-party reactive library** — `github.com/ReactiveX/RxGo`
   (push-based observables) or `github.com/influxdata/tdigest`-style
   stream processors. RxGo provides observable composition; automatic
   dependency tracking is still partial.
3. **Pull-based memo graph** — an explicit DAG of `Signal[T]` values
   with `Computed[T]` derived nodes; the operator declares
   dependencies at construction (which makes "automatic tracking"
   semi-automatic-from-a-declared-list rather than runtime-inferred).

The pattern's "automatic dependency tracking" invariant is structurally
hard to satisfy in Go without a code-generation step (a generator that
walks the derived expression body and emits the upstream subscription
list). Designs choosing reactive-form in Go should be honest: the
automatic-tracking property is partial. The example below uses the
channel-goroutine vehicle because it is most idiomatic Go; reactive
forms via RxGo are referenced separately.

```go
package reactive

import "sync"

// Signal is a state cell with subscribers. Writing triggers
// dependent re-evaluation.
type Signal[T any] struct {
	mu          sync.RWMutex
	value       T
	subscribers []func(T)
}

func NewSignal[T any](initial T) *Signal[T] {
	return &Signal[T]{value: initial}
}

func (s *Signal[T]) Get() T {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.value
}

func (s *Signal[T]) Set(v T) {
	s.mu.Lock()
	s.value = v
	subs := append([]func(T){}, s.subscribers...)
	s.mu.Unlock()
	for _, sub := range subs {
		sub(v)
	}
}

func (s *Signal[T]) Subscribe(f func(T)) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.subscribers = append(s.subscribers, f)
}

// Computed derives a value from upstream signals. Dependencies are
// declared at construction (deps slice). The expression body must
// be pure: it reads only the declared deps via their Get() and
// returns a value.
type Computed[T any] struct {
	*Signal[T]
}

func NewComputed[T any, U any](
	dep *Signal[U],
	expr func(U) T,
) *Computed[T] {
	c := &Computed[T]{Signal: NewSignal(expr(dep.Get()))}
	dep.Subscribe(func(v U) { c.Signal.Set(expr(v)) })
	return c
}

// Effect is the leaf side-effect node: subscribes to a signal and
// performs IO when it changes. Effects re-run only on change, not
// on a fixed schedule.
func Effect[T any](sig *Signal[T], action func(T)) {
	action(sig.Get())
	sig.Subscribe(action)
}

// Example:
//
//   firstName := NewSignal("Alice")
//   greeting := NewComputed(firstName,
//       func(n string) string { return "Hello, " + n })
//   Effect(greeting.Signal, func(g string) { fmt.Println(g) })
//   firstName.Set("Bob")  // triggers greeting recompute + print
```

### Probes

- **Signal type at state boundaries (AST).** State that participates
  in the reactive graph must be typed as `*Signal[T]` (or the
  project's named equivalent), not as plain `T`. AST probe: for each
  function parameter or field of a reactive component, verify the
  type is the Signal carrier.
- **Derived expressions are pure (AST).** Walk each expression
  passed to `NewComputed` and flag any `*ast.CallExpr` whose receiver
  is a `*Signal` other than the declared upstream (cross-signal
  reads not in the dependency list = staleness bug) or any call to a
  known IO package. Derived expressions read declared upstreams and
  nothing else.
- **Effects do not appear inside derived expressions.** Walk each
  `NewComputed` expression body for calls to `Effect` or direct IO
  (`fmt.Print`, `http.Get`, `os.WriteFile`). Effects belong at the
  leaves, subscribed via `Effect(...)`; inside a Computed expression
  they break purity and cause re-execution surprises.
- **No `setInterval`-equivalent polling.** Search for
  `time.NewTicker`, `time.After`, or `time.Tick` driving downstream
  re-evaluation. If a goroutine re-reads a signal on a fixed
  schedule, the change-driven semantics is bypassed — that is polling
  rendered as reactive-form.
- **No manual `Render()` / `Update()` calls.** Walk the codebase for
  explicit invocations of `Render()`, `Update()`, `Recompute()` that
  the operator calls after a state change. If they exist, the
  framework is not owning propagation; the operator is, and the
  pattern's "declare relationships, not control flow" invariant is
  broken.
- **Glitch-free convergence (runtime probe).** Test: subscribe to a
  derived value via `Effect(...)`, then call `Set` on two upstream
  signals in rapid succession (within the same goroutine), and
  assert the effect ran ≤ 2 times, not 4. Naive implementations
  re-evaluate per upstream change without batching; reactive-form
  guarantees one re-evaluation per dependent per stable upstream
  state. (Note: the example above does *not* batch; production
  implementations need an explicit batching layer to satisfy this
  probe.)
- **Dependency-declaration completeness.** For each Computed, walk
  the expression body's free variables and verify each `Signal` read
  inside the body is declared in the `deps` parameter. Reads of
  signals not in the declared dependency list cause silent staleness
  — flag.

### Example

The Go community lacks a dominant reactive-form library; the closest
references are `github.com/ReactiveX/RxGo` for observable composition
and `github.com/influxdata/flux`'s data-flow runtime. For pull-based
reactive memoization, `golang.org/x/sync/singleflight` partially
overlaps (deduplicating concurrent reads of the same key) but is not
a full reactive graph. Channel-goroutine graphs as shown above are
the most idiomatic vehicle but require operator-declared
dependencies.

### Anti-patterns

- **Polling goroutine re-reading state on a ticker.** A `for range
  time.Tick(100 * time.Millisecond) { if sig.Get() != lastValue
  { ... } }` loop is polling, not reactive. The Get / Set boundary
  must trigger propagation; ticker-driven re-reads bypass it.
- **Mutating signal state in a Computed expression.** A Computed
  body that calls `otherSignal.Set(...)` is doing a side effect inside
  a derivation — re-evaluation order becomes nondeterministic, cycles
  become possible, and the runtime cannot guarantee glitch-free
  convergence.
- **Observer struct with manually-coded `Notify()` methods.** A
  struct holding state in `self` fields plus a `Notify()` method the
  state-mutator calls is the GoF Observer pattern, not reactive-form.
  The framework-owned dependency graph is the defining feature.
- **Computed reading a package-level mutable variable.** A Computed
  expression body referencing `someGlobal` (not a declared upstream
  signal) introduces a staleness vector — when `someGlobal` changes
  with no corresponding signal-write, the Computed does not
  re-evaluate. All reads inside a Computed must be Signal-typed.

## Render: Ada

**Applicability: marginal.** Ada projects in scope for this catalog
(safety-critical, embedded, aerospace, defence) are characteristically
*not* UI projects, and Ada has no in-language equivalent of SolidJS /
MobX / Recoil — the framework-owned dependency graph that defines the
pattern. RFC #590 (and parent Issue #594) flags this pattern explicitly
as "not-applicable" for Ada at the project-portfolio level.

A constrained Ada rendering exists for cases where reactive semantics
are required — sensor-fusion modules where derived telemetry values
must re-compute when raw signals change. The vehicle is a protected
type per signal, with subscribers registered as access-to-procedure
values. This is a hand-rolled observer chain, not a true reactive
runtime: the glitch-free convergence and automatic dependency tracking
invariants from the language-neutral Shape are *not* satisfied by this
rendering, and the pattern's judgment probes (automatic dependency
tracking, batched updates) will mark such code as failing the pattern.

If a project needs glitch-free reactive semantics in Ada, the
recommendation is to express the dataflow as a synchronous block-diagram
in a higher-level tool (Simulink, SCADE) that generates Ada — moving the
pattern out of source and into a generative layer.

```ada
--  Constrained protected-type rendering. Implements signal updates +
--  subscriber notification, but does NOT satisfy the pattern's
--  automatic-dependency-tracking or glitch-free invariants. Use only
--  when sensor-fusion / signal-aggregation needs are narrow enough
--  that hand-rolled observer chains suffice.

generic
   type Value is private;
package Signals is

   type Subscriber is access procedure (V : Value);

   protected type Signal is
      procedure Set (V : Value);
      function  Get return Value;
      procedure Subscribe   (S : Subscriber);
      procedure Unsubscribe (S : Subscriber);
   private
      Current : Value;
      Subs    : array (1 .. 16) of Subscriber := (others => null);
   end Signal;

end Signals;
```

A derived signal would be expressed as a `Subscribe` callback that
reads upstream signals via their `Get` accessors and writes to a
downstream signal via `Set` — entirely manual; dependency tracking
is in the operator's head, not in the framework. The judgment probes
"Automatic dependency tracking" and "Glitch-free under concurrent
changes" will report failure on this rendering — which is the
correct signal that reactive-form is the wrong pattern choice for
Ada-domain projects.

### Probes

- **Project-level applicability marker present.** Before running
  pattern-specific probes, check the project's sidecar
  `language: ada` and `pattern: reactive-form` combination — the
  detector should emit a `pattern-applicability: not-recommended`
  warning rather than a per-rule pass / fail. (Sibling Issue #595's
  sidecar spec exposes this; until that lands the detector should
  hard-code the language-pattern combination as a warning.)
- **No `with Ada.Real_Time` polling timer in subscriber bodies.**
  If a Subscribe callback reads from `Ada.Real_Time.Clock` and only
  recomputes when the clock advances rather than when upstream
  changes, the change-driven semantics is bypassed — the rendering
  has collapsed to polling. Walk subscriber bodies; flag any
  `Ada.Real_Time` references.
- **Protected type's `Set` invokes all subscribers atomically.** If
  `Set` updates `Current` and then iterates `Subs` outside the
  protected operation's mutual exclusion, the update is not
  glitch-free. Locate the protected body's `Set` procedure; assert
  the subscriber-invocation loop lives inside the protected
  operation's body (not in a separate callback registered to fire
  after `Set` returns).
- **No `Subscriber'Access` of a procedure that itself calls `Set`
  on its own signal.** Reactive frameworks detect and reject
  cycles; this hand-rolled rendering does not. The detector should
  walk subscriber bodies and flag direct cycles statically — the
  full reactive invariant cannot be checked, but obvious self-edges
  can.

### Example

N/A — synthesized example. AdaCore's `GNATCOLL.Refcount` example
shows protected-type-based reference counting, which is the closest
in-tree Ada code shape to the protected-type-as-signal rendering.
SCADE Suite (commercial, Esterel Technologies / Ansys) and the
open-source Sireum HAMR project are the closest "reactive-form in
Ada" tooling — both generate Ada from a higher-level synchronous
specification rather than expressing reactivity in source.

### Anti-patterns

- **Periodic task that re-evaluates all derived values.** A task
  `task body Recomputer is begin loop delay 0.1; Recompute_All; end
  loop; end Recomputer;` — Ada's tasking primitive used for
  polling. Looks reactive (a task watches state), is actually
  time-triggered.
- **Mutually-recursive protected operations forming a hidden
  graph.** Signal A's Set calls B's Set which calls A's Get; the
  dependency edges exist but are scattered across protected type
  bodies. The "graph" is in the operator's head; the detector
  cannot enumerate it. This is exactly the failure mode the
  pattern's framework-owned graph invariant exists to prevent.
- **Naive observer chain via package-level callback lists.** A
  package `Signals` exporting `Subscribe : access procedure` and a
  flat array of callbacks — calls happen in registration order,
  not topological order, so a derived signal may observe upstream
  values that are about to change in the same tick. The glitch-free
  convergence invariant is violated; the rendering is a GoF
  Observer, not reactive-form.

## Render: TypeScript

**This pattern's home.** The project's real reactive-form consumer is
the SolidJS UI (`mcp-server/ui/src/`): `createSignal` leaves,
`createMemo` derived nodes, JSX as the terminal derived values. The
#986 retrofit corrected the catalog applicability from `[]` to
`[typescript]` — with TypeScript a probed language, the marker now
names the one language where the pattern genuinely applies
(python/go/ada combos remain off-applicability, judgment-only).

```typescript
import { createSignal, createMemo } from "solid-js";

const [query, setQuery] = createSignal("");
const [rows, setRows] = createSignal<ReadonlyArray<Row>>([]);

// Derived nodes: pure expressions over upstream signals.
const filtered = createMemo(() =>
  rows().filter((r) => r.title.includes(query())),
);
const count = createMemo(() => filtered().length);

// The framework re-evaluates filtered/count when query or rows
// change; the operator declared relationships, not control flow.
```

### Probes

Mechanical (wired in `_TYPESCRIPT_PROBES`):

- **derived_values_are_memos.** A function declared reactive-form
  exercises the signal-graph API (`createSignal` / `createMemo` /
  `createComputed`); derived state computed with neither is manual
  control flow wearing the declaration.
- **no_signal_write_in_derivation.** No setter call (`setX(...)`)
  inside a `createMemo(...)` body — a derivation that writes a signal
  is a cycle-in-waiting and breaks the pure-expression invariant
  (Forecloses §Imperative recompute).

Judgment (prose-only):

- **derived state is never also set.** A value computed by a memo is
  not additionally assigned in event handlers.
- **leaves are the only writes.** Event handlers write input signals;
  everything downstream derives.

### Example

`mcp-server/ui/src/` throughout — the swimlane board's filtered /
grouped / counted views derive from fetch-payload and filter-input
signals via `createMemo`.

### Anti-patterns

- **`setFiltered(rows().filter(...))` inside an event handler.**
  Manual re-set of derived state — the graph no longer owns the
  recomputation; staleness bugs follow.
- **Memo bodies calling setters.** A derivation with a write is
  control flow, not a node.
