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.