# Speckle Agents Data Validator

> >

- **Type:** Skill
- **Install:** `agentstack add skill-impertio-studio-speckle-claude-skill-package-speckle-agents-data-validator`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Impertio-Studio](https://agentstack.voostack.com/s/impertio-studio)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Impertio-Studio](https://github.com/Impertio-Studio)
- **Source:** https://github.com/Impertio-Studio/Speckle-Claude-Skill-Package/tree/master/skills/source/speckle-agents/speckle-agents-data-validator

## Install

```sh
agentstack add skill-impertio-studio-speckle-claude-skill-package-speckle-agents-data-validator
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# speckle-agents-data-validator

## Validation Decision Tree

```
Need to validate Speckle data?
|
+-- BEFORE sending (pre-flight)?
|   +-- Single object → OBJECT SCHEMA CHECK (Section 1)
|   +-- Geometry object → GEOMETRY INTEGRITY (Section 2)
|   +-- Object with displayValue → DISPLAY VALUE CHECK (Section 3)
|   +-- Root collection → COLLECTION TREE CHECK (Section 4)
|   +-- Root with proxies → PROXY REFERENCE CHECK (Section 5)
|   +-- Cross-object units → UNIT CONSISTENCY (Section 6)
|   +-- Full commit payload → FULL PRE-FLIGHT (Section 7)
|
+-- AFTER receiving (post-receive)?
|   +-- Received objects missing data → POST-RECEIVE AUDIT (Section 8)
|   +-- Proxy references unresolvable → PROXY RESOLUTION (Section 9)
|   +-- Geometry renders incorrectly → GEOMETRY AUDIT (Section 10)
|
+-- Continuous quality auditing?
    +-- Model-wide property coverage → COMPLETENESS AUDIT (Section 11)
    +-- Cross-version drift detection → VERSION DIFF (Section 12)
```

---

## Section 1: Object Schema Validation

**When:** ALWAYS validate every Base object before adding it to a commit payload.

### Required Properties Checklist

| Property | Required | Validation Rule |
|----------|----------|-----------------|
| `speckle_type` | YES | MUST be a non-empty string. NEVER send with `speckle_type = ""` or `None`. |
| `id` | NO (pre-send) | NEVER set `id` manually. It is computed during serialization. |
| `applicationId` | CONDITIONAL | MUST be set for round-trip tracking. Unique within one source app only. |
| `units` | CONDITIONAL | MUST be set on ALL geometry objects. Values: `"m"`, `"mm"`, `"cm"`, `"ft"`, `"in"`. |

### Dynamic Property Name Rules

- NEVER use empty strings as property names
- NEVER use names containing `.` or `/`
- NEVER use names with consecutive `@@`
- Names starting with `@` trigger detachment during serialization
- Names starting with `__` are excluded from hashing and serialization

### Python Validation Pattern

```python
def validate_base_object(obj: Base) -> list[str]:
    errors = []
    if not getattr(obj, "speckle_type", None):
        errors.append("speckle_type is missing or empty")
    if obj.id is not None and not isinstance(obj.id, str):
        errors.append("id must be None (pre-send) or a string (post-receive)")
    for name in obj.get_dynamic_member_names():
        if not name:
            errors.append("Empty dynamic property name found")
        if "." in name or "/" in name:
            errors.append(f"Invalid chars in property name: '{name}'")
        if "@@" in name:
            errors.append(f"Consecutive @@ in property name: '{name}'")
    return errors
```

---

## Section 2: Geometry Integrity Validation

**When:** ALWAYS validate geometry objects before sending. Malformed geometry causes viewer crashes.

### Mesh Validation Checklist

| Check | Rule | Consequence of Failure |
|-------|------|----------------------|
| Vertices length | MUST be divisible by 3 | Viewer crash or silent data loss |
| Faces structure | Each face: count `n`, then `n` indices | Garbled rendering, missing faces |
| Face index bounds | Every index MUST be `= 3 | Degenerate faces |
| Units present | `units` MUST be set | Wrong scale rendering |
| Non-empty arrays | `vertices` and `faces` length > 0 | Empty mesh wastes bandwidth |

### Python Mesh Validator

```python
def validate_mesh(mesh: Mesh) -> list[str]:
    errors = []
    if not mesh.vertices or len(mesh.vertices) == 0:
        errors.append("Mesh has no vertices"); return errors
    if len(mesh.vertices) % 3 != 0:
        errors.append(f"Vertices length {len(mesh.vertices)} not divisible by 3")
    vertex_count = len(mesh.vertices) // 3
    if not mesh.faces or len(mesh.faces) == 0:
        errors.append("Mesh has no faces"); return errors
    i, face_idx = 0, 0
    while i = len(mesh.faces):
            errors.append(f"Face {face_idx}: overflows faces array"); break
        for j in range(1, n + 1):
            idx = mesh.faces[i + j]
            if idx = vertex_count:
                errors.append(f"Face {face_idx}: index {idx} out of bounds")
        i += n + 1; face_idx += 1
    if not getattr(mesh, "units", None):
        errors.append("Mesh has no units set")
    return errors
```

### Optional Array Validation

| Array | Rule |
|-------|------|
| `colors` | Length MUST equal vertex count (one ARGB int per vertex) |
| `textureCoordinates` | Length MUST equal `vertex_count * 2` (U, V per vertex) |
| `vertexNormals` | Length MUST equal `len(vertices)` (x, y, z per vertex) |

---

## Section 3: displayValue Validation

**When:** ALWAYS validate `displayValue` on DataObjects before sending. Missing `displayValue` makes objects invisible in the viewer.

### Rules

- MUST be a list (NEVER a single object, NEVER `None` for visual objects)
- Every element MUST be a geometry type (Mesh, Line, Point, Polyline, Curve, Arc, Circle, Polycurve, Brep)
- Every element MUST have `units` set
- NEVER nest DataObjects inside `displayValue`

```python
GEOMETRY_TYPES = {
    "Objects.Geometry.Mesh", "Objects.Geometry.Line", "Objects.Geometry.Point",
    "Objects.Geometry.Polyline", "Objects.Geometry.Curve", "Objects.Geometry.Arc",
    "Objects.Geometry.Circle", "Objects.Geometry.Polycurve", "Objects.Geometry.Brep",
}

def validate_display_value(obj: Base) -> list[str]:
    errors = []
    dv = getattr(obj, "displayValue", None)
    if dv is None:
        return ["displayValue is None -- object invisible in viewer"]
    if not isinstance(dv, list):
        return [f"displayValue must be a list, got {type(dv).__name__}"]
    if len(dv) == 0:
        errors.append("displayValue is empty -- object invisible in viewer")
    for i, geom in enumerate(dv):
        if not isinstance(geom, Base):
            errors.append(f"displayValue[{i}]: not a Base object"); continue
        st = getattr(geom, "speckle_type", "")
        if st not in GEOMETRY_TYPES:
            errors.append(f"displayValue[{i}]: '{st}' is not a geometry type")
        if not getattr(geom, "units", None):
            errors.append(f"displayValue[{i}]: missing units")
    return errors
```

---

## Section 4: Collection Tree Validation

**When:** ALWAYS validate the root collection before sending. Cycles cause infinite recursion during serialization.

### Rules

- Collections MUST form a strict directed tree (no cycles, no shared parents)
- Collection `name` MUST be a non-empty string
- Collection `elements` MUST be a list

```python
def validate_collection_tree(root) -> list[str]:
    errors, visited, path = [], set(), set()
    def walk(node, depth=0):
        nid = id(node)
        if nid in path:
            errors.append(f"Cycle at depth {depth}: {getattr(node, 'name', '?')}"); return
        if nid in visited:
            errors.append(f"Shared parent: '{getattr(node, 'name', '?')}'"); return
        visited.add(nid); path.add(nid)
        for child in getattr(node, "elements", []) or []:
            if hasattr(child, "elements"):
                walk(child, depth + 1)
        path.discard(nid)
    walk(root)
    return errors
```

---

## Section 5: Proxy Reference Integrity

**When:** ALWAYS validate proxy references before sending. Broken references cause silent data loss.

### Rules

- Every `applicationId` in a proxy's `objects` list MUST exist in the commit's object tree
- Proxies MUST reference objects by `applicationId`, NEVER by `id`
- No proxy MUST reference another proxy
- `InstanceProxy.definitionId` MUST match an `InstanceDefinitionProxy.applicationId`

### Validation Steps

1. Walk the object tree and collect all `applicationId` values into a set
2. For each proxy, verify every entry in `objects` exists in that set
3. For each `InstanceProxy`, verify `definitionId` matches an `InstanceDefinitionProxy`
4. Report all unresolvable references

See [references/examples.md](references/examples.md) for the complete implementation.

---

## Section 6: Unit Consistency Validation

**When:** ALWAYS check units across all geometry in a commit. Mixed units cause wrong-scale rendering.

### Rules

- ALL geometry objects in a single-source commit SHOULD use the same units
- Supported values: `"m"`, `"mm"`, `"cm"`, `"ft"`, `"in"`
- NEVER send geometry without units
- Mixed units in federated models are acceptable but MUST be documented

```python
def validate_unit_consistency(objects: list) -> list[str]:
    errors, units_found = [], {}
    for obj in objects:
        unit = getattr(obj, "units", None)
        if unit is None:
            errors.append(f"'{getattr(obj, 'speckle_type', '?')}' has no units")
        elif unit not in ("m", "mm", "cm", "ft", "in"):
            errors.append(f"Unknown unit '{unit}'")
        else:
            units_found[unit] = units_found.get(unit, 0) + 1
    if len(units_found) > 1:
        errors.append(f"Mixed units: {units_found}. Verify intentional.")
    return errors
```

---

## Section 7: Full Pre-Flight Validation

**When:** ALWAYS run before `operations.send()`. Orchestrates all individual validators.

### Checklist

1. Validate collection tree structure (Section 4)
2. Walk all objects -- validate schema (1), geometry (2), displayValue (3)
3. Validate proxy references (Section 5)
4. Validate unit consistency (Section 6)
5. Report: total objects, total errors, errors by category

```python
def pre_flight_validate(root_collection, proxies: list) -> dict:
    all_errors, geometry_objects = [], []
    stats = {"objects": 0, "geometry": 0}
    all_errors.extend(validate_collection_tree(root_collection))
    def walk(node):
        stats["objects"] += 1
        all_errors.extend(validate_base_object(node))
        st = getattr(node, "speckle_type", "")
        if st in GEOMETRY_TYPES:
            stats["geometry"] += 1; geometry_objects.append(node)
            if "Mesh" in st: all_errors.extend(validate_mesh(node))
        if hasattr(node, "displayValue"):
            all_errors.extend(validate_display_value(node))
        for child in getattr(node, "elements", []) or []:
            walk(child)
    walk(root_collection)
    all_errors.extend(validate_proxy_references(root_collection, proxies))
    all_errors.extend(validate_unit_consistency(geometry_objects))
    return {"errors": all_errors, "stats": stats}
```

---

## Section 8: Post-Receive Validation

**When:** ALWAYS validate received data when downstream workflows depend on specific properties.

### Post-Receive Checklist

| Check | Why |
|-------|-----|
| `displayValue` populated | Connectors MAY strip geometry on load |
| Expected `speckle_type` present | Data may have been converted by intermediate tool |
| `applicationId` unique | Duplicates break proxy resolution |
| `properties` has expected keys | Not all connectors preserve properties on round-trip |
| Geometry has `units` | Older SDKs may omit units |

### Duplicate applicationId Detection

```python
def check_duplicate_app_ids(objects: list) -> list[str]:
    errors, seen = [], {}
    for obj in objects:
        app_id = getattr(obj, "applicationId", None)
        if app_id:
            if app_id in seen:
                errors.append(f"Duplicate '{app_id}': {seen[app_id]} and {getattr(obj, 'speckle_type', '?')}")
            else:
                seen[app_id] = getattr(obj, "speckle_type", "?")
    return errors
```

---

## Section 9: Proxy Resolution After Receive

**When:** After receiving data with proxies, ALWAYS verify references resolve.

Build a map of `applicationId -> object` from all received objects. For each proxy, look up every entry in its `objects` list. Log unresolvable references. See [references/examples.md](references/examples.md) for the `resolve_proxies()` implementation.

---

## Section 10: Geometry Audit After Receive

**When:** Received geometry renders incorrectly in the viewer.

```
Geometry renders incorrectly
|
+-- Invisible? → Check displayValue (Section 3), vertices/faces, units
+-- Wrong scale? → Check units field, unit mismatch (Section 6)
+-- Missing faces? → Run mesh integrity (Section 2), check face encoding
+-- No colors/materials? → Check proxy resolution (Section 9), mesh.colors length
```

---

## Section 11: Completeness Audit

**When:** Model-wide QA -- verify all objects meet minimum property standards.

Pass a list of objects and required property keys to `audit_property_coverage()`. Returns per-key coverage percentages and lists of objects missing each property. See [references/examples.md](references/examples.md) for the implementation and usage pattern.

---

## Section 12: Cross-Version Drift Detection

**When:** Compare two model versions to detect unexpected changes.

Match objects by `applicationId` across versions. Objects with same `applicationId` but different `id` (content hash) are modified. Objects present in only one version are added or removed. See [references/examples.md](references/examples.md) for the `detect_drift()` implementation.

---

## Critical Warnings

**NEVER** set `id` manually on a Base object. The `id` is a content-addressed hash computed during serialization. Manual assignment causes hash mismatches and data corruption.

**NEVER** modify an object after calling `get_id()`. Any property change invalidates the hash. ALWAYS finalize all properties before computing the ID.

**NEVER** use `id` in proxy `objects` lists. Proxies reference by `applicationId`. Using `id` breaks resolution because `id` changes on every content update.

**NEVER** create cycles in collection hierarchies. Collections MUST form a strict directed tree. Cycles cause infinite recursion during serialization.

**NEVER** omit `units` on geometry objects. The viewer has no reliable default. Objects without units render at unpredictable scales.

**NEVER** assume `applicationId` is globally unique. It is unique only within a single source application.

**ALWAYS** validate mesh face encoding. Each face starts with vertex count `n` followed by `n` indices. One off-by-one error corrupts all subsequent faces.

**ALWAYS** run pre-flight validation before `operations.send()`. Catching errors before transport prevents storing malformed data on the server.

---

## Reference Links

- [references/methods.md](references/methods.md) -- Validation method signatures for Python and C#
- [references/examples.md](references/examples.md) -- Complete validation workflow examples
- [references/anti-patterns.md](references/anti-patterns.md) -- What NOT to do when validating Speckle data

### Official Sources

- https://docs.speckle.systems/developers/data-schema/overview
- https://docs.speckle.systems/developers/data-schema/object-schema
- https://docs.speckle.systems/developers/data-schema/proxy-schema
- https://speckle.guide/dev/base.html

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Impertio-Studio](https://github.com/Impertio-Studio)
- **Source:** [Impertio-Studio/Speckle-Claude-Skill-Package](https://github.com/Impertio-Studio/Speckle-Claude-Skill-Package)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-impertio-studio-speckle-claude-skill-package-speckle-agents-data-validator
- Seller: https://agentstack.voostack.com/s/impertio-studio
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
