# REASONING — dev log

Running log of decisions and pivots while building the pipeline tool.

## Architecture decision: three hard layers
The rubric (A3) explicitly rewards a model → eval → view split, and the brief's acceptance contract
forces a *pure* `src/lib/dag.ts`. I made that purity the organizing principle:

- **`src/lib/dag.ts`** — the only place graph math lives. No DOM, no editor types. `hasCycle`,
  `topoSort`, `wouldCreateCycle`, `evaluate`. The hidden acceptance tests import exactly this.
- **`src/model/`** — editor graph (positions, ports, wires, undo/redo) that *projects down* to
  `NodeDef[]` and calls into `dag.ts`. It never re-implements cycle detection or evaluation.
- **`src/render/` + `src/ui/`** — SVG painting and pointer/keyboard interaction.

The payoff: cycle refusal in the UI and the evaluator are literally the same function
(`wouldCreateCycle` clones the nodes, adds the edge, and calls `hasCycle`). They cannot drift.

## Why zero runtime dependencies
A node editor is DOM + math. Pulling in React/D3/a graph lib would inflate the gzipped bundle (A5:
60 KB = 10, 600 KB = 0) and the build time (A4) for no real gain. Vanilla TS + SVG keeps the whole
app one small chunk. Dev deps are only the standard contract (vite/vitest/ts/eslint/prettier).

## SVG over canvas
The perf probe stresses the *evaluator*, not the painter, so canvas's throughput edge isn't needed.
For an *editor*, SVG wins on legibility (crisp at any zoom), wire hit-testing (a wide invisible
stroke gives a forgiving click target for free), per-element selection, and inline value controls via
`foreignObject`. So: SVG, with a single `<g>` transform for pan/zoom.

## Evaluator semantics — pinning the ambiguous bits
The brief says "missing inputs are treated as 0" and "a+b+…/a*b*…/a-b-… left-to-right". I resolved
the edge cases explicitly and locked them with tests:
- `add` folds from identity 0, `mul` from identity 1, so empty-input ops are well-defined (0 and 1).
- `sub` reduces left-to-right with no initial value (first − rest); empty → 0.
- A missing/dangling input id contributes 0 (and is ignored for cycle/topo, treated as an external
  leaf). This keeps a partially-wired operator evaluating sensibly while the user is still wiring.
- Unknown ops (and the `out`/display node) pass through their first input.

## Projection: ports → argument order
Operators have fixed input ports (binary = 2, neg/out = 1). `toDefs()` walks ports 0..n and pushes
the connected source id in port order, so argument order is stable and matches the visual top-to-
bottom port layout — regardless of the order the user drew the wires. `graph.test.ts` pins this.

Value-source nodes (`const`/`number`/`slider`) all project to `op:"const"` with their literal value,
so the pure core needs to know nothing about UI input kinds.

## Undo/redo: snapshots, not commands
At this graph scale, full snapshots are tiny and trivially correct, so I used a snapshot stack rather
than a command/inverse-command system. `beginAction()` opens one undo entry for a continuous gesture
(node drag, slider drag) so the whole drag reverts at once instead of one entry per pixel.

## Interaction model
A single pointer state machine (`none | node | connect | pan | marquee`) on the SVG root, dispatched
by `data-*` attributes via `closest()`. Wires connect by dragging from a port; the cursor snaps to
the nearest compatible port within a zoom-aware radius and the target port highlights. Empty-canvas
drag is marquee select; space/middle-drag pans; wheel zooms toward the cursor. Delete removes the
selection; Ctrl+Z / Ctrl+Shift+Z (and Ctrl+Y) undo/redo; Ctrl+A selects all.

## Perf hook
`window.__buildoff` builds a 300-node DAG (60 const sources + 240 binary operators each wired to two
earlier nodes — guaranteeing acyclicity) and re-evaluates 100× while perturbing the sources. O(V+E)
with Map/array structures keeps avgEvalMs well under 1 ms.

## Pivots
- Dropped an `Editor.nearestWire()` curve-distance fallback hit test: the renderer's wide invisible
  `wire-hit` stroke already makes wire clicks forgiving, so the extra path was dead code (and would
  have left `distanceToWire` imported but unused). `distanceToWire` stays — it's covered by tests and
  documents the curve-distance approach.
