# DAG Client Integration Contract **Status:** Draft **Last Updated:** 2026-02-13 This document describes what silo-mod and Headless Create runners need to implement to integrate with the Silo dependency DAG and worker system. --- ## 1. Overview The DAG system has two client-side integration points: 1. **silo-mod workbench** (desktop) -- pushes DAG data to Silo on save or revision create. 2. **silorunner + silo-mod** (headless) -- extracts DAGs, validates features, and exports geometry as compute jobs. Both share the same Python codebase in the silo-mod repository. Desktop users call the code interactively; runners call it headlessly via `create --console`. --- ## 2. DAG Sync Payload Clients push feature trees to Silo via: ``` PUT /api/items/{partNumber}/dag Authorization: Bearer Content-Type: application/json ``` ### 2.1 Request Body ```json { "revision_number": 3, "nodes": [ { "node_key": "Sketch001", "node_type": "sketch", "properties_hash": "a1b2c3d4e5f6...", "metadata": { "label": "Base Profile", "constraint_count": 12 } }, { "node_key": "Pad001", "node_type": "pad", "properties_hash": "f6e5d4c3b2a1...", "metadata": { "label": "Main Extrusion", "length": 25.0 } } ], "edges": [ { "source_key": "Sketch001", "target_key": "Pad001", "edge_type": "depends_on" } ] } ``` ### 2.2 Field Reference **Nodes:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `node_key` | string | yes | Unique within item+revision. Use Create's internal object name (e.g. `Sketch001`, `Pad003`). | | `node_type` | string | yes | One of: `sketch`, `pad`, `pocket`, `fillet`, `chamfer`, `constraint`, `body`, `part`, `datum`. | | `properties_hash` | string | no | SHA-256 hex digest of the node's parametric inputs. Used for memoization. | | `validation_state` | string | no | One of: `clean`, `dirty`, `validating`, `failed`. Defaults to `clean`. | | `metadata` | object | no | Arbitrary key-value pairs for display or debugging. | **Edges:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `source_key` | string | yes | The node that is depended upon. | | `target_key` | string | yes | The node that depends on the source. | | `edge_type` | string | no | One of: `depends_on` (default), `references`, `constrains`. | **Direction convention:** Edges point from dependency to dependent. If Pad001 depends on Sketch001, the edge is `source_key: "Sketch001"`, `target_key: "Pad001"`. ### 2.3 Response ```json { "synced": true, "node_count": 15, "edge_count": 14 } ``` --- ## 3. Computing properties_hash The `properties_hash` enables memoization -- if a node's inputs haven't changed since the last validation, it can be skipped. Computing it: ```python import hashlib import json def compute_properties_hash(feature_obj): """Hash the parametric inputs of a Create feature.""" inputs = {} if feature_obj.TypeId == "Sketcher::SketchObject": # Hash geometry + constraints inputs["geometry_count"] = feature_obj.GeometryCount inputs["constraint_count"] = feature_obj.ConstraintCount inputs["geometry"] = str(feature_obj.Shape.exportBrep()) elif feature_obj.TypeId == "PartDesign::Pad": inputs["length"] = feature_obj.Length.Value inputs["type"] = str(feature_obj.Type) inputs["reversed"] = feature_obj.Reversed inputs["sketch"] = feature_obj.Profile[0].Name # ... other feature types canonical = json.dumps(inputs, sort_keys=True) return hashlib.sha256(canonical.encode()).hexdigest() ``` The exact inputs per feature type are determined by what parametric values affect the feature's geometry. Include anything that, if changed, would require recomputation. --- ## 4. Feature Tree Walking To extract the DAG from a Create document: ```python import FreeCAD def extract_dag(doc): """Walk a Create document and return nodes + edges.""" nodes = [] edges = [] for obj in doc.Objects: # Skip non-feature objects if not hasattr(obj, "TypeId"): continue node_type = classify_type(obj.TypeId) if node_type is None: continue nodes.append({ "node_key": obj.Name, "node_type": node_type, "properties_hash": compute_properties_hash(obj), "metadata": { "label": obj.Label, "type_id": obj.TypeId, } }) # Walk dependencies via InList (objects this one depends on) for dep in obj.InList: if hasattr(dep, "TypeId") and classify_type(dep.TypeId): edges.append({ "source_key": dep.Name, "target_key": obj.Name, "edge_type": "depends_on", }) return nodes, edges def classify_type(type_id): """Map Create TypeIds to DAG node types.""" mapping = { "Sketcher::SketchObject": "sketch", "PartDesign::Pad": "pad", "PartDesign::Pocket": "pocket", "PartDesign::Fillet": "fillet", "PartDesign::Chamfer": "chamfer", "PartDesign::Body": "body", "Part::Feature": "part", "Sketcher::SketchConstraint": "constraint", } return mapping.get(type_id) ``` --- ## 5. When to Push DAG Data Push the DAG to Silo in these scenarios: | Event | Trigger | Who | |-------|---------|-----| | User saves in silo-mod | On save callback | Desktop silo-mod workbench | | User creates a revision | After `POST /api/items/{pn}/revisions` succeeds | Desktop silo-mod workbench | | Runner extracts DAG | After `create-dag-extract` job completes | silorunner via `PUT /api/runner/jobs/{id}/dag` | | Runner validates | After `create-validate` job, push updated validation states | silorunner via `PUT /api/runner/jobs/{id}/dag` | --- ## 6. Runner Entry Points silo-mod must provide these Python entry points for headless invocation: ### 6.1 silo.runner.dag_extract Extracts the feature DAG from a Create file and writes it as JSON. ```python # silo/runner.py def dag_extract(input_path, output_path): """ Extract feature DAG from a Create file. Args: input_path: Path to the .kc (Kindred Create) file. output_path: Path to write the JSON output. Output JSON format: { "nodes": [...], // Same format as DAG sync payload "edges": [...] } """ doc = FreeCAD.openDocument(input_path) nodes, edges = extract_dag(doc) with open(output_path, 'w') as f: json.dump({"nodes": nodes, "edges": edges}, f) FreeCAD.closeDocument(doc.Name) ``` ### 6.2 silo.runner.validate Rebuilds all features and reports pass/fail per node. ```python def validate(input_path, output_path): """ Validate a Create file by rebuilding all features. Output JSON format: { "valid": true/false, "nodes": [ { "node_key": "Pad001", "state": "clean", // or "failed" "message": null, // error message if failed "properties_hash": "..." } ] } """ doc = FreeCAD.openDocument(input_path) doc.recompute() results = [] all_valid = True for obj in doc.Objects: if not hasattr(obj, "TypeId"): continue node_type = classify_type(obj.TypeId) if node_type is None: continue state = "clean" message = None if hasattr(obj, "isValid") and not obj.isValid(): state = "failed" message = f"Feature {obj.Label} failed to recompute" all_valid = False results.append({ "node_key": obj.Name, "state": state, "message": message, "properties_hash": compute_properties_hash(obj), }) with open(output_path, 'w') as f: json.dump({"valid": all_valid, "nodes": results}, f) FreeCAD.closeDocument(doc.Name) ``` ### 6.3 silo.runner.export Exports geometry to STEP, IGES, or other formats. ```python def export(input_path, output_path, format="step"): """ Export a Create file to an external format. Args: input_path: Path to the .kc file. output_path: Path to write the exported file. format: Export format ("step", "iges", "stl", "obj"). """ doc = FreeCAD.openDocument(input_path) import Part shapes = [obj.Shape for obj in doc.Objects if hasattr(obj, "Shape")] compound = Part.makeCompound(shapes) format_map = { "step": "STEP", "iges": "IGES", "stl": "STL", "obj": "OBJ", } Part.export([compound], output_path) FreeCAD.closeDocument(doc.Name) ``` --- ## 7. Headless Invocation The `silorunner` binary shells out to Create (with silo-mod installed): ```bash # DAG extraction create --console -e "from silo.runner import dag_extract; dag_extract('/tmp/job/part.kc', '/tmp/job/dag.json')" # Validation create --console -e "from silo.runner import validate; validate('/tmp/job/part.kc', '/tmp/job/result.json')" # Export create --console -e "from silo.runner import export; export('/tmp/job/part.kc', '/tmp/job/output.step', 'step')" ``` **Prerequisites:** The runner host must have: - Headless Create installed (Kindred's fork of FreeCAD) - silo-mod installed as a Create addon (so `from silo.runner import ...` works) - No display server required -- `--console` mode is headless --- ## 8. Validation Result Handling After a runner completes a `create-validate` job, it should: 1. Read the result JSON. 2. Push updated validation states via `PUT /api/runner/jobs/{jobID}/dag`: ```json { "revision_number": 3, "nodes": [ {"node_key": "Sketch001", "node_type": "sketch", "validation_state": "clean", "properties_hash": "abc..."}, {"node_key": "Pad001", "node_type": "pad", "validation_state": "failed", "properties_hash": "def..."} ], "edges": [ {"source_key": "Sketch001", "target_key": "Pad001"} ] } ``` 3. Complete the job via `POST /api/runner/jobs/{jobID}/complete` with the summary result. --- ## 9. SSE Events Clients should listen for these events on `GET /api/events`: | Event | Payload | When | |-------|---------|------| | `dag.updated` | `{item_id, part_number, revision_number, node_count, edge_count}` | After any DAG sync | | `dag.validated` | `{item_id, part_number, valid, failed_count}` | After validation completes | | `job.created` | `{job_id, definition_name, trigger, item_id}` | Job auto-triggered or manually created | | `job.claimed` | `{job_id, runner_id, runner}` | Runner claims a job | | `job.progress` | `{job_id, progress, message}` | Runner reports progress | | `job.completed` | `{job_id, runner_id}` | Job finishes successfully | | `job.failed` | `{job_id, runner_id, error}` | Job fails | | `job.cancelled` | `{job_id, cancelled_by}` | Job cancelled by user | --- ## 10. Cross-Item Edges For assembly constraints that reference geometry in child parts (e.g. a mate constraint between two parts), use the `dag_cross_edges` table. These edges bridge the BOM DAG and the feature DAG. Cross-item edges are **not** included in the standard `PUT /dag` sync. They will be managed through a dedicated endpoint in a future iteration once the assembly constraint model in Create/silo-mod is finalized. For now, the DAG sync covers intra-item dependencies only. Assembly-level interference detection uses the BOM DAG (`relationships` table) combined with per-item feature DAGs.