spec(client): DAG push and edit session contract for silo-mod / Kindred Create #168

Open
opened 2026-03-01 15:40:05 +00:00 by forbes · 0 comments
Owner

Context

Sub-issue of #125 (Context-Aware Part Subscription System).

The server-side edit session and checkpoint infrastructure (#161-#167) depends on the client (silo-mod FreeCAD extension and Kindred Create) implementing specific behaviors. This issue specifies the contract so client development can proceed in parallel.

What the Server Provides

1. Workstation Registration

On first launch, the client must register its workstation:

POST /api/workstations
{
  "name": "alice-desktop",
  "fingerprint": "<stable-machine-id>"
}

The fingerprint must be stable across restarts. Recommended: hash of platform.node() + uuid.getnode() or OS machine-id. Registration is idempotent — same fingerprint returns the existing workstation.

Store the returned workstation_id in the client config.

2. SSE Connection with Workstation Identity

Connect to SSE with workstation_id:

GET /api/events?workstation_id={workstation_id}

This associates the connection with the workstation for targeted event delivery.

3. Edit Session Lifecycle

The client must map its internal editing context transitions to server session calls.

Context Detection

The client needs to detect when the user enters and exits these contexts:

User Action Context Level Object ID Server Call
Opens Sketcher on Sketch001 sketch Sketch001 Acquire
Closes Sketcher (exiting sketch) Sketch001 Push checkpoint + release
Enters PartDesign on Body partdesign Body Acquire
Leaves PartDesign (exiting partdesign) Body Push checkpoint + release
Opens Assembly editing assembly Assembly Acquire
Closes Assembly editing (exiting assembly) Assembly Push checkpoint + release

Acquire Call

POST /api/items/{partNumber}/edit-sessions
{
  "workstation_id": "uuid",
  "context_level": "sketch",
  "object_id": "Sketch001",
  "dependency_cone": ["Sketch001", "Pad003", "Fillet005"]
}

dependency_cone is optional but strongly recommended. The client should compute it from the local feature tree:

  • Starting from the object being edited
  • Walk the parametric dependency graph forward (downstream)
  • Collect all feature names that depend on this object

If the client cannot compute the cone, omit it — the server will try to compute it from the stored DAG.

Handle Acquire Response

{"session_id": "uuid", "interference": "none", "conflicts": []}
  • interference: "none" — proceed normally
  • interference: "soft" — warn the user but allow entry. Show which user and object conflicts.
  • HTTP 409 (hard_interference) — block entry. Show who holds the lock and when they acquired it.

The client must store the session_id for the release call.

Release Call (via Checkpoint)

On upward context transition, push a checkpoint that atomically releases the session:

POST /api/items/{partNumber}/checkpoints

With the checkpoint file/diff and session_id in the request.

4. DAG Push

The client should push the feature DAG after any structural change:

PUT /api/items/{partNumber}/dag
{
  "revision_number": 3,
  "nodes": [
    {"node_key": "Sketch001", "node_type": "sketch", "properties_hash": "abc123"},
    {"node_key": "Pad003", "node_type": "feature", "properties_hash": "def456"},
    {"node_key": "Fillet005", "node_type": "feature", "properties_hash": "ghi789"}
  ],
  "edges": [
    {"source_node_id": "Sketch001", "target_node_id": "Pad003", "edge_type": "depends_on"},
    {"source_node_id": "Pad003", "target_node_id": "Fillet005", "edge_type": "depends_on"}
  ]
}

When to push:

  • On formal commit (revision creation)
  • On checkpoint push (include dag field in checkpoint request)
  • After structural changes (new feature, deleted feature, reorder)

Node types to include:

  • sketch — Sketch objects
  • feature — PartDesign features (Pad, Pocket, Fillet, etc.)
  • body — Body containers
  • part — Part containers
  • assembly — Assembly containers
  • constraint — Assembly constraints

Edge semantics:

  • depends_on — target depends on source (source is upstream)
  • Direction: source → target means "target depends on source"

properties_hash: SHA-256 of the serialized feature properties. Used for change detection — if the hash changes, the node and its forward cone are marked dirty.

5. Checkpoint Content

The client pushes checkpoint content in one of two modes:

Diff Mode (preferred for XML parametric source)

Compute a unified diff of the XML parametric source against the base revision:

import difflib
base_xml = get_revision_xml(base_revision)
current_xml = get_current_xml()
patch = difflib.unified_diff(base_xml.splitlines(), current_xml.splitlines())

Upload as storage_mode: "diff" with base_revision set.

Full Mode (fallback)

Upload the complete file. Use when:

  • Content is binary (mesh data, images)
  • Diff would be larger than 50% of full file
  • Base revision is not available locally

6. SSE Events to Handle

Event Client Action
edit.session_acquired Show presence indicator ("Alice is editing Sketch001")
edit.session_released Remove presence indicator
edit.interference_resolved Update/dismiss soft interference warning
edit.handoff_requested Show notification to user ("Bob is requesting access to Sketch001")
edit.force_released Warn user their session was force-released, save local work

7. Dependency Cone Computation

Pseudocode for computing the forward cone from the FreeCAD document:

def compute_forward_cone(doc, object_name):
    """Walk downstream from object_name, collecting all dependent features."""
    cone = set()
    queue = [object_name]
    while queue:
        current = queue.pop(0)
        if current in cone:
            continue
        cone.add(current)
        obj = doc.getObject(current)
        if obj is None:
            continue
        # Find all objects that reference this one
        for other in doc.Objects:
            if current in [ref.Name for ref in other.OutList if ref is not None]:
                # 'other' depends on 'current'
                queue.append(other.Name)
    return list(cone)

Note: FreeCAD's OutList gives upstream dependencies. We need to invert: find all objects whose OutList includes the current object. Alternatively, build a reverse adjacency map once and walk it.

Acceptance Criteria (Client Side)

  • Client registers workstation on first launch with stable fingerprint
  • SSE connection includes workstation_id query param
  • Context transitions map to acquire/release calls
  • Hard interference (409) blocks entry to editing context
  • Soft interference shows warning but allows entry
  • Checkpoints pushed on upward context transitions
  • DAG pushed on commit and checkpoint
  • Dependency cone computed from local feature tree
  • SSE presence events shown in UI

Repos

  • silo-mod — FreeCAD Silo extension (Python)
  • Kindred Create — Assembly workbench and core modifications

Depends On

  • Server issues #161-#167 define the API contract
  • Client can start development against the spec before server is complete (mock/stub endpoints)

Part Of

#125

## Context Sub-issue of #125 (Context-Aware Part Subscription System). The server-side edit session and checkpoint infrastructure (#161-#167) depends on the client (silo-mod FreeCAD extension and Kindred Create) implementing specific behaviors. This issue specifies the contract so client development can proceed in parallel. ## What the Server Provides ### 1. Workstation Registration On first launch, the client must register its workstation: ``` POST /api/workstations { "name": "alice-desktop", "fingerprint": "<stable-machine-id>" } ``` The `fingerprint` must be stable across restarts. Recommended: hash of `platform.node() + uuid.getnode()` or OS machine-id. Registration is idempotent — same fingerprint returns the existing workstation. Store the returned `workstation_id` in the client config. ### 2. SSE Connection with Workstation Identity Connect to SSE with workstation_id: ``` GET /api/events?workstation_id={workstation_id} ``` This associates the connection with the workstation for targeted event delivery. ### 3. Edit Session Lifecycle The client must map its internal editing context transitions to server session calls. #### Context Detection The client needs to detect when the user enters and exits these contexts: | User Action | Context Level | Object ID | Server Call | |-------------|--------------|-----------|-------------| | Opens Sketcher on Sketch001 | `sketch` | `Sketch001` | Acquire | | Closes Sketcher | (exiting sketch) | `Sketch001` | Push checkpoint + release | | Enters PartDesign on Body | `partdesign` | `Body` | Acquire | | Leaves PartDesign | (exiting partdesign) | `Body` | Push checkpoint + release | | Opens Assembly editing | `assembly` | `Assembly` | Acquire | | Closes Assembly editing | (exiting assembly) | `Assembly` | Push checkpoint + release | #### Acquire Call ``` POST /api/items/{partNumber}/edit-sessions { "workstation_id": "uuid", "context_level": "sketch", "object_id": "Sketch001", "dependency_cone": ["Sketch001", "Pad003", "Fillet005"] } ``` **dependency_cone** is optional but strongly recommended. The client should compute it from the local feature tree: - Starting from the object being edited - Walk the parametric dependency graph forward (downstream) - Collect all feature names that depend on this object If the client cannot compute the cone, omit it — the server will try to compute it from the stored DAG. #### Handle Acquire Response ```json {"session_id": "uuid", "interference": "none", "conflicts": []} ``` - `interference: "none"` — proceed normally - `interference: "soft"` — warn the user but allow entry. Show which user and object conflicts. - HTTP 409 (`hard_interference`) — **block entry**. Show who holds the lock and when they acquired it. The client must store the `session_id` for the release call. #### Release Call (via Checkpoint) On upward context transition, push a checkpoint that atomically releases the session: ``` POST /api/items/{partNumber}/checkpoints ``` With the checkpoint file/diff and `session_id` in the request. ### 4. DAG Push The client should push the feature DAG after any structural change: ``` PUT /api/items/{partNumber}/dag { "revision_number": 3, "nodes": [ {"node_key": "Sketch001", "node_type": "sketch", "properties_hash": "abc123"}, {"node_key": "Pad003", "node_type": "feature", "properties_hash": "def456"}, {"node_key": "Fillet005", "node_type": "feature", "properties_hash": "ghi789"} ], "edges": [ {"source_node_id": "Sketch001", "target_node_id": "Pad003", "edge_type": "depends_on"}, {"source_node_id": "Pad003", "target_node_id": "Fillet005", "edge_type": "depends_on"} ] } ``` **When to push:** - On formal commit (revision creation) - On checkpoint push (include `dag` field in checkpoint request) - After structural changes (new feature, deleted feature, reorder) **Node types to include:** - `sketch` — Sketch objects - `feature` — PartDesign features (Pad, Pocket, Fillet, etc.) - `body` — Body containers - `part` — Part containers - `assembly` — Assembly containers - `constraint` — Assembly constraints **Edge semantics:** - `depends_on` — target depends on source (source is upstream) - Direction: source → target means "target depends on source" **properties_hash:** SHA-256 of the serialized feature properties. Used for change detection — if the hash changes, the node and its forward cone are marked dirty. ### 5. Checkpoint Content The client pushes checkpoint content in one of two modes: #### Diff Mode (preferred for XML parametric source) Compute a unified diff of the XML parametric source against the base revision: ```python import difflib base_xml = get_revision_xml(base_revision) current_xml = get_current_xml() patch = difflib.unified_diff(base_xml.splitlines(), current_xml.splitlines()) ``` Upload as `storage_mode: "diff"` with `base_revision` set. #### Full Mode (fallback) Upload the complete file. Use when: - Content is binary (mesh data, images) - Diff would be larger than 50% of full file - Base revision is not available locally ### 6. SSE Events to Handle | Event | Client Action | |-------|---------------| | `edit.session_acquired` | Show presence indicator ("Alice is editing Sketch001") | | `edit.session_released` | Remove presence indicator | | `edit.interference_resolved` | Update/dismiss soft interference warning | | `edit.handoff_requested` | Show notification to user ("Bob is requesting access to Sketch001") | | `edit.force_released` | Warn user their session was force-released, save local work | ### 7. Dependency Cone Computation Pseudocode for computing the forward cone from the FreeCAD document: ```python def compute_forward_cone(doc, object_name): """Walk downstream from object_name, collecting all dependent features.""" cone = set() queue = [object_name] while queue: current = queue.pop(0) if current in cone: continue cone.add(current) obj = doc.getObject(current) if obj is None: continue # Find all objects that reference this one for other in doc.Objects: if current in [ref.Name for ref in other.OutList if ref is not None]: # 'other' depends on 'current' queue.append(other.Name) return list(cone) ``` Note: FreeCAD's `OutList` gives upstream dependencies. We need to invert: find all objects whose `OutList` includes the current object. Alternatively, build a reverse adjacency map once and walk it. ## Acceptance Criteria (Client Side) - [ ] Client registers workstation on first launch with stable fingerprint - [ ] SSE connection includes `workstation_id` query param - [ ] Context transitions map to acquire/release calls - [ ] Hard interference (409) blocks entry to editing context - [ ] Soft interference shows warning but allows entry - [ ] Checkpoints pushed on upward context transitions - [ ] DAG pushed on commit and checkpoint - [ ] Dependency cone computed from local feature tree - [ ] SSE presence events shown in UI ## Repos - `silo-mod` — FreeCAD Silo extension (Python) - Kindred Create — Assembly workbench and core modifications ## Depends On - Server issues #161-#167 define the API contract - Client can start development against the spec before server is complete (mock/stub endpoints) ## Part Of #125
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/silo#168