openquiver-graph 0.1.0

OpenQuiver graph IR: nodes, pins, types. Bottom layer, no internal dependencies.
Documentation
# openquiver-graph

The graph IR: nodes, pins, wires. Pure data, no I/O, and — the invariant that
actually matters — no dependency on any other **workspace** crate: nothing
here reaches into the parser, the compiler, the runtime or the editor. That
says nothing about ordinary external crates, and this one has several
(`thiserror`, the `unicode-*` pair for identifier normalization). Two of
them, `serde` and `ts-rs`, are there because something outside this crate
needs the IR's shape in a form it did not have to hand-write. Neither lets
this crate reach outward; both only let other things reach in.

## Why it exists

Everything in OpenQuiver — the parser, the compiler, the runtime, the editor —
works against this one representation. The file on disk is a *projection* of
this IR, never the other way around, which is what makes evolving the storage
format a `load → save` with no data migrations.

## Design decisions worth knowing

**A node's identity is its name.** There are no hidden ids. That is what lets a
readable DSL refer to a node as `ask` rather than `01J8X…`. The price is that
renaming is a refactor that rewrites every reference — the same deal any
programming language makes.

**Wires come in two kinds and the distinction is the product.** `EdgeKind::Exec`
is the white wire that orders steps; `EdgeKind::Data` carries values. Separating
them is what lets a workflow say "first A, then B" without inventing a fake data
dependency, which is the thing dataflow builders cannot express.

**Collections are keyed, never positional.** Nodes live in a `BTreeMap` keyed by
name, edges in a `BTreeSet`. Two reasons, both load-bearing:

- *Canonical order.* Nodes sort alphabetically and edges sort exec-block first,
  then data — so a saved file is byte-identical regardless of the order things
  were created in, and concurrent insertions by two people land in different
  parts of the file instead of colliding at the end.
- *Mergeability.* No positional arrays and no insertion-order ids means the
  document can later be backed by a CRDT (Automerge, Loro) for live
  co-editing without rewriting the editor. Live sync is not being built now;
  keeping the door open costs nothing today and a rewrite later.

`EdgeKind`'s variant order and `Edge`'s field order are both load-bearing —
they are what the derived `Ord` uses to produce the canonical edge order.

## Comments live here too

`Trivia` holds ordinary `//` comments keyed by the element they describe, and it
sits beside the IR rather than in the syntax crate for one reason: the keys are
IR identities, so renaming a node has to update the graph and the comment table
in a single operation. A table one layer above the code performing the rename
cannot be kept in step with it.

`///` is different — it is the node's description and its editor tooltip, so it
is a real IR field (`Node::doc`), not trivia.

## Deliberately not here

Resolved pin *types*. Data types are derived by the compiler through wildcard
propagation, so storing them on an edge would create a second source of truth
that can go stale.

## Why `Value` and `DataType` derive `TS`

`openquiver-editor`'s view — what the canvas draws, and what crosses into the
TypeScript frontend as generated bindings — embeds `Value` and `DataType`
directly: a node's configuration literals and a pin's data type are exactly
this crate's `Value` and `DataType`, not a lookalike. Deriving `ts-rs`'s `TS`
here, beside the type it describes, is what lets the frontend see that shape
at all.

The alternative was a second enum in the editor — `ValueView`, say, hand-kept
in sync with `Value`'s variants — purely so that *something* in that crate
could derive `TS` without reaching back into `openquiver-graph`. That is the same
fact told twice: every time `Value` gained or changed a variant, the mirror
would need the same edit made by hand a second time, in a different file, in
a different crate, and nothing would fail loudly the day someone forgot. The
whole reason this project keeps resolved pin types out of the IR (see above)
is to avoid exactly that shape of bug. Deriving `TS` in place costs one
`#[derive(TS)]` and one external dependency; the mirror would have cost a
permanent, silent way for the frontend's idea of `Value` to drift from the
real one. `ts-rs` crosses the crate boundary the same way `serde` already
does — it does not create a new kind of coupling, only a second consumer of
one that was already there.