# @zakkster/lite-studio

> In-page visual debugger for @zakkster/lite-signal reactive graphs. A pure CONSUMER
> of @zakkster/lite-devtools (the data layer). Renders a live layered DAG (SVG),
> a node inspector, engine stats, a lifecycle log, and DOT export, as a draggable
> overlay. Non-self-instrumenting: it never creates a signal, so it adds zero nodes
> to the graph it inspects. ESM only. Zero runtime deps of its own.
> Peers: @zakkster/lite-devtools ^1.0.0, @zakkster/lite-time ^1.0.0 (lite-signal transitive).

## Architecture (separation of concerns)

  lite-signal   -> reactive engine + introspection (describe/forEach*/nodeId/stats)
  lite-devtools -> walks/inspects the graph (graph/inspect/monitor/track/toDot)
  lite-studio   -> renders + interacts (this package). Consumes devtools only.
  lite-time     -> every() drives the poll cadence

lite-studio computes nothing about the graph itself. It is also a GHOST: all of its
state lives in plain variables and the DOM is updated imperatively. It never calls
signal()/computed()/effect(), so it does not pollute activeNodes or trip leakWatch.

## Exports

- mount(roots, options?) -> controller     // roots: handle | handle[]; the only entry point

options: { cadenceMs?: number (default 300), title?: string }
controller: {
  refresh()        // re-read the graph now; relayout only if structure changed
  select(id|null)  // select a node by stable id (or clear)
  unmount()        // remove panel, stop cadence, dispose listeners (idempotent)
}

## What it renders

- Graph view: layered DAG, sources left -> observers right. signal=ellipse,
  computed=box, effect=diamond; colour by kind; live value in each node.
  Layout is STABLE -- recomputed only on node/edge-set change; value changes update
  text in place and flash. Large graphs scroll (overflow:auto). No pan/zoom in v1.
- Inspector: click a node -> kind/id/value/observed + dependencies + observers as
  clickable chips (navigate the graph).
- Engine stats: live monitor() (signals/computeds/effects/activeLinks/activeNodes/pooledLinks).
- Lifecycle log: nodes added/removed across ticks + root connect/disconnect via track().
- Copy DOT: toDot(graph(roots)) to the clipboard.

## Update model

Polls graph()/monitor() on a lite-time `every` cadence (~300ms). Structure signature
(sorted node ids + edges) is diffed each tick: structural change -> relayout + redraw;
otherwise -> in-place value refresh + flash. No synchronous hooks on the user's links.

## Gotchas / design notes

- Non-self-instrumenting is a hard constraint: studio uses NO signals internally.
- studio reads a node's value from the graph() DESCRIPTOR's own `value` field, NOT via
  inspect().value (which uses peek() and returns undefined on a plain descriptor).
- Effects carry no value -> rendered without a value line.
- Relayout is gated on structure only; value churn never moves nodes.
- Requires a DOM; mount() throws otherwise. Tested headless with happy-dom.

## Not in v1

Pan/zoom (scroll instead) and force-directed layout are intentionally omitted.
A Chrome DevTools extension panel could reuse this renderer later, fed bridged
JSON snapshots from a page agent instead of live handles.

## Constraints

- ESM only ("type": "module"). Node >= 18 for the test harness; runtime is the browser.
- Peers: @zakkster/lite-devtools, @zakkster/lite-time. No runtime deps of its own.
- Files are ASCII. License MIT (c) Zahary Shinikchiev.
