Skip to content

CU1 — Creating a graph

g = somatize.Graph()
g.node("clean", Clean())
g.edge("clean", "vectorize")

Status: closed. 16 tests in Rust, 13 in Python.

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.rsa_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

Validationdecided: 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).

  1. The core’s Graph is 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 in python/. That is why core depends on nothing.
  2. Errors at insertion, not in a validate(). There is no instant at which the graph is malformed.
  3. 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.
  4. NodeId is a type, not a String. There are more ids coming.
  5. 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.

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.