- Remove hand-crafted kindred-icons/ in favor of auto-generated themed icons - Add icons/mappings/ with FCAD.csv (Tango palette) and kindred.csv (Catppuccin Mocha) - Add icons/retheme.py script to remap upstream FreeCAD SVG colors - Generate icons/themed/ with 1,595 themed SVGs (45,300 color replacements) - BitmapFactory loads icons/themed/ as highest priority before default icons - 157-color mapping covers the full Tango palette, interpolating between 4 luminance anchors per color family Regenerate: python3 icons/retheme.py
12 KiB
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:
- silo-mod workbench (desktop) -- pushes DAG data to Silo on save or revision create.
- 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 <user_token or runner_token>
Content-Type: application/json
2.1 Request Body
{
"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
{
"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:
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:
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.
# 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.
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.
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):
# 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 --
--consolemode is headless
8. Validation Result Handling
After a runner completes a create-validate job, it should:
- Read the result JSON.
- Push updated validation states via
PUT /api/runner/jobs/{jobID}/dag:
{
"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"}
]
}
- Complete the job via
POST /api/runner/jobs/{jobID}/completewith 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.