DAG.md describes the two-tier dependency graph (BOM DAG + feature DAG), node/edge data model, validation states, dirty propagation, forward/backward cone queries, DAG sync payload format, and REST API. WORKERS.md describes the general-purpose async compute job system: YAML job definitions, job lifecycle (pending→claimed→running→completed/failed), runner registration and authentication, claim semantics (SELECT FOR UPDATE SKIP LOCKED), timeout enforcement, SSE events, and REST API.
9.7 KiB
Dependency DAG Specification
Status: Draft Last Updated: 2026-02-13
1. Purpose
The Dependency DAG is a server-side graph that tracks how features, constraints, and assembly relationships depend on each other. It enables three capabilities described in MULTI_USER_EDITS.md:
- Interference detection -- comparing dependency cones of concurrent edit sessions to classify conflicts as none, soft, or hard before the user encounters them.
- Incremental validation -- marking changed nodes dirty and propagating only through the affected subgraph, using input-hash memoization to stop early when inputs haven't changed.
- Structured merge safety -- walking the DAG to determine whether concurrent edits share upstream dependencies, deciding if auto-merge is safe or manual review is required.
2. Two-Tier Model
Silo maintains two levels of dependency graph:
2.1 BOM DAG (existing)
The assembly-to-part relationship graph already stored in the relationships table. Each row represents a parent item containing a child item with a quantity and relationship type (component, alternate, reference). This graph is queried via GetBOM, GetExpandedBOM, GetWhereUsed, and HasCycle in internal/db/relationships.go.
The BOM DAG is not modified by this specification. It continues to serve its existing purpose.
2.2 Feature DAG (new)
A finer-grained graph stored in dag_nodes and dag_edges tables. Each node represents a feature within a single item's revision -- a sketch, pad, fillet, pocket, constraint, body, or part-level container. Edges represent "depends on" relationships: if Pad003 depends on Sketch001, an edge runs from Sketch001 to Pad003.
The feature DAG is populated by clients (silo-mod) when users save, or by runners after compute jobs. Silo stores and queries it but does not generate it -- the Create client has access to the feature tree and is the authoritative source.
2.3 Cross-Item Edges
Assembly constraints often reference geometry on child parts (e.g., "mate Face6 of PartA to Face2 of PartB"). These cross-item dependencies are stored in dag_cross_edges, linking a node in one item to a node in another. Each cross-edge optionally references the relationships row that establishes the BOM connection.
3. Data Model
3.1 dag_nodes
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
item_id |
UUID | FK to items.id |
revision_number |
INTEGER | Revision this DAG snapshot belongs to |
node_key |
TEXT | Feature name from Create (e.g., Sketch001, Pad003, Body) |
node_type |
TEXT | One of: sketch, pad, pocket, fillet, chamfer, constraint, body, part, datum, mirror, pattern, boolean |
properties_hash |
TEXT | SHA-256 of the node's parametric inputs (sketch coordinates, fillet radius, constraint values). Used for memoization -- if the hash hasn't changed, validation can skip this node. |
validation_state |
TEXT | One of: clean, dirty, validating, failed |
validation_msg |
TEXT | Error message when validation_state = 'failed' |
metadata |
JSONB | Type-specific data (sketch coords, feature params, constraint definitions) |
created_at |
TIMESTAMPTZ | Row creation time |
updated_at |
TIMESTAMPTZ | Last state change |
Uniqueness: (item_id, revision_number, node_key) -- one node per feature per revision.
3.2 dag_edges
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
source_node_id |
UUID | FK to dag_nodes.id -- the upstream node |
target_node_id |
UUID | FK to dag_nodes.id -- the downstream node that depends on source |
edge_type |
TEXT | depends_on (default), references, constrains |
metadata |
JSONB | Optional edge metadata |
Direction convention: An edge from A to B means "B depends on A". A is upstream, B is downstream. Forward-cone traversal from A walks edges where A is the source.
Uniqueness: (source_node_id, target_node_id, edge_type).
Constraint: source_node_id != target_node_id (no self-edges).
3.3 dag_cross_edges
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
source_node_id |
UUID | FK to dag_nodes.id -- node in item A |
target_node_id |
UUID | FK to dag_nodes.id -- node in item B |
relationship_id |
UUID | FK to relationships.id (nullable) -- the BOM entry connecting the two items |
edge_type |
TEXT | assembly_ref (default) |
metadata |
JSONB | Reference details (face ID, edge ID, etc.) |
Uniqueness: (source_node_id, target_node_id).
4. Validation States
Each node has a validation_state that tracks whether its computed geometry is current:
| State | Meaning |
|---|---|
clean |
Node's geometry matches its properties_hash. No recompute needed. |
dirty |
An upstream change has propagated to this node. Recompute required. |
validating |
A compute job is currently revalidating this node. |
failed |
Recompute failed. validation_msg contains the error. |
4.1 State Transitions
clean → dirty (upstream change detected, or MarkDirty called)
dirty → validating (compute job claims this node)
validating → clean (recompute succeeded, properties_hash updated)
validating → failed (recompute produced an error)
failed → dirty (upstream change detected, retry possible)
dirty → clean (properties_hash matches previous -- memoization shortcut)
4.2 Dirty Propagation
When a node is marked dirty, all downstream nodes in its forward cone are also marked dirty. This is done atomically in a single recursive CTE:
WITH RECURSIVE forward_cone AS (
SELECT $1::uuid AS node_id
UNION
SELECT e.target_node_id
FROM dag_edges e
JOIN forward_cone fc ON fc.node_id = e.source_node_id
)
UPDATE dag_nodes SET validation_state = 'dirty', updated_at = now()
WHERE id IN (SELECT node_id FROM forward_cone)
AND validation_state = 'clean';
4.3 Memoization
Before marking a node dirty, the system can compare the new properties_hash against the stored value. If they match, the change did not affect this node's inputs, and propagation stops. This is the memoization boundary described in MULTI_USER_EDITS.md Section 5.2.
5. Graph Queries
5.1 Forward Cone
Returns all nodes downstream of a given node -- everything that would be affected if the source node changes. Used for interference detection: if two users' forward cones overlap, there is potential interference.
WITH RECURSIVE forward_cone AS (
SELECT target_node_id AS node_id
FROM dag_edges WHERE source_node_id = $1
UNION
SELECT e.target_node_id
FROM dag_edges e
JOIN forward_cone fc ON fc.node_id = e.source_node_id
)
SELECT n.* FROM dag_nodes n JOIN forward_cone fc ON n.id = fc.node_id;
5.2 Backward Cone
Returns all nodes upstream of a given node -- everything the target node depends on.
5.3 Dirty Subgraph
Returns all nodes for a given item where validation_state != 'clean', along with their edges. This is the input to an incremental validation job.
5.4 Cycle Detection
Before adding an edge, check that it would not create a cycle. Uses the same recursive ancestor-walk pattern as HasCycle in internal/db/relationships.go.
6. DAG Sync
Clients push the full feature DAG to Silo via PUT /api/items/{partNumber}/dag. The sync payload is a JSON document:
{
"revision": 3,
"nodes": [
{
"key": "Sketch001",
"type": "sketch",
"properties_hash": "a1b2c3...",
"metadata": {
"coordinates": [[0, 0], [10, 0], [10, 5]],
"constraints": ["horizontal", "vertical"]
}
},
{
"key": "Pad003",
"type": "pad",
"properties_hash": "d4e5f6...",
"metadata": {
"length": 15.0,
"direction": [0, 0, 1]
}
}
],
"edges": [
{
"source": "Sketch001",
"target": "Pad003",
"type": "depends_on"
}
]
}
The server processes this within a single transaction:
- Upsert all nodes (matched by
item_id + revision_number + node_key). - Replace all edges for this item/revision.
- Compare new
properties_hashvalues against stored values to detect changes. - Mark changed nodes and their forward cones dirty.
- Publish
dag.updatedSSE event.
7. Interference Detection
When a user registers an edit context (MULTI_USER_EDITS.md Section 3.1), the server:
- Looks up the node(s) being edited by
node_keywithin the item's current revision. - Computes the forward cone for those nodes.
- Compares the cone against all active edit sessions' cones.
- Classifies interference:
- No overlap → no interference, fully concurrent.
- Overlap, different objects → soft interference, visual indicator via SSE.
- Same object, same edit type → hard interference, edit blocked.
8. REST API
All endpoints are under /api/items/{partNumber} and require authentication.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/dag |
viewer | Get full feature DAG for current revision |
GET |
/dag/forward-cone/{nodeKey} |
viewer | Get forward dependency cone |
GET |
/dag/dirty |
viewer | Get dirty subgraph |
PUT |
/dag |
editor | Sync full feature tree (from client or runner) |
POST |
/dag/mark-dirty/{nodeKey} |
editor | Manually mark a node and its cone dirty |
9. References
- MULTI_USER_EDITS.md -- Full multi-user editing specification
- WORKERS.md -- Worker/runner system that executes validation jobs
- ROADMAP.md -- Tier 0 Dependency DAG entry