CU1 — Creating a graph
g = somatize.Graph()g.node("clean", Clean())g.edge("clean", "vectorize")Status: closed. 16 tests in Rust, 13 in Python.
The design decision that comes first
Section titled “The design decision that comes first”Before node() you have to answer what a node is. In the original that
question was answered with NodeKind (5 structural variants), NodeMeta
(metadata common to filters and steps, with cacheable/deterministic as data
rather than an if is_step) and a NodeCatalog that is the single registry. It
is a reasonable answer; it is not the only one, and it is not inherited.
What is worth looking at in the original before deciding, because they are scars
from real mistakes: soma-legacy/soma-core/src/graph/node.rs (172 lines) and its tests
graph_node.rs — a_filter_keeps_its_caching_contract,
a_step_is_not_output_cacheable, schemas_survive_both_directions.
Questionnaire (from soma-legacy/soma-core/tests/unit/graph*.rs)
Section titled “Questionnaire (from soma-legacy/soma-core/tests/unit/graph*.rs)”Construction
- an empty graph is valid
- a single-node graph is valid
- a node can be added with an explicit id
- a node can be added without an id and the system gives it one (snake_case of the class)
- adding the same thing twice does not duplicate — decided: identity = id,
and the derived id suffixes
_2,_3. Two identical filters are two nodes; deduplicating by content is a caching decision, not a topology one, and that use case does not exist - a linear pipeline has the structure it says it has
Topology queries
- roots and leaves
- a node’s predecessors and successors
- topological order of a linear chain
- topological order with parallel branches
- a cycle is an error — decided: when the edge is added, not when it is walked
Validation — decided: there is no validate(). The constructors return
Result and the invariant holds at all times, so an invalid Graph is not a
value that exists. What it buys: topological_sort() does not return Result,
because it cannot fail.
- duplicate ids are rejected, in
add_node - an edge to a non-existent node is rejected, in
add_edge
Deferred to later use cases — it is in the same test files, do not drag it
into CU1: serialization (graph_serde_roundtrip), rendering (to_mermaid*,
to_text, overlays), control nodes (loop_and_branch_nodes, subgraph_node,
all of graph_control.rs), and the Filter/Step contract (graph_filter.rs,
graph_step.rs).
Decisions taken in CU1
Section titled “Decisions taken in CU1”- The core’s
Graphis topology only. Ids and edges. What a node does is none of its business, because creating a graph does not need to know. The id → Python object map lives inpython/. That is whycoredepends on nothing. - Errors at insertion, not in a
validate(). There is no instant at which the graph is malformed. - DAG by construction. The cycle is rejected in
add_edge. Risk taken: if a future use case needed back edges, it would have to be revisited — in the original, loops are nodes, not backward edges, so the bet is that it is not needed. NodeIdis a type, not aString. There are more ids coming.- O(n) where it could be O(1). Adjacency is computed on the fly. The code reads at a glance and no use case has asked for anything else.
What did NOT go in, and why
Section titled “What did NOT go in, and why”target= in node(). I wrote it as sugar for creating the edge in the same
step, and checking it against the original turned up that there target is
not an edge: it is the supervision target the optimizer’s step() reads.
Reusing the name with another meaning is exactly the kind of thing that makes a
system incomprehensible, so out it went: it is not CU1 and it has no consumer
today.