← Back to Main Documentation Core Systems Index

Spatial Relationships System

Created: Original date unknown
Updated: Tue 29 Jul 2025 07:36:45 CEST - Renamed from spatial-relationships-system.md and added Jekyll front matter
Document Version: 1.1 - Updated during file structure reorganization
Security Classification: Internal Technical Documentation
Target Audience: Backend Developers, Database Engineers, System Architects
Author: Paul Wisén

This document defines how objects relate to each other in physical space using flexible, predicate-based relationships. The spatial system tracks both where objects are (current location) and where they should be (expected location) to enable inventory management and misplacement detection.

Core Concepts

Spatial Predicates

Relationships like IN, ON, NEXT_TO, etc. that describe physical positioning between objects. The available predicates are defined in the Supabase spatial_predicates table as the single source of truth, and are stored as directed relationships in Neo4j.

Predicate Architecture

Dual Spatial Predicate System (from Supabase)

The system uses dual predicates to separately track current and expected/normal locations with full spatial detail:

Containment Predicates (axis: containment)

Support Predicates (axis: support)

Horizontal Positioning Predicates (axis: horizontal)

Vertical Positioning Predicates (axis: vertical)

Attachment Predicates (axis: attachment)

Legacy Predicate (Deprecated)

UI Dropzone System Mapping: The spatial dashboard maps Supabase predicates to visual dropzones:

Spatial Parent-Child Relationships

Defining Spatial Parents

A spatial parent is an object that directly contains or supports another object, creating a parent-child relationship where the child object follows the parent when moved.

Parent-Creating Relationships (child follows parent):

Non-Parent (Positional) Relationships (objects remain independent):

Container Pattern with Parent Relationships

Objects can contain other objects through parent-creating relationships, creating spatial hierarchies:

// Parent-child chain (each arrow represents a parent relationship)
(:Keys)-[:IN]->(:Box)-[:ON]->(:Shelf)-[:IN]->(:Room)

// Movement propagation: Move Room → Shelf follows → Box follows → Keys follows

Warehouse Pallet Example

// Parent relationships (objects follow parent when moved)
(:Box1)-[:ON]->(:Pallet)
(:Box2)-[:ON]->(:Pallet)
(:Pallet)-[:IN]->(:WarehouseZone)

// Positional relationships (objects don't follow parent when moved)
(:Pallet2)-[:LEFT_OF]->(:Pallet)
(:Shelf)-[:ABOVE]->(:Pallet)

// Spatial hierarchy for Box1: [Box1, Pallet, WarehouseZone]
// Note: Pallet2 and Shelf are NOT in Box1's spatial hierarchy

Current vs Normal Location Model

The system distinguishes between:

This enables powerful inventory auditing workflows that compare actual vs expected locations with full spatial relationship detail.

Misplacement Detection Examples

Same Container, Correct Placement:

(:Keys)-[:CURRENT_IN]->(:KeyDrawer)
(:Keys)-[:NORMAL_IN]->(:KeyDrawer)
// Status: ✅ Keys are in correct location

Same Container, Wrong Positioning:

(:Monitor)-[:CURRENT_LEFT_OF]->(:Computer)
(:Monitor)-[:NORMAL_RIGHT_OF]->(:Computer)
// Status: ⚠️ Monitor is on wrong side of computer

Wrong Container:

(:Box)-[:CURRENT_ON]->(:Pallet_A)
(:Box)-[:NORMAL_ON]->(:Pallet_B)
// Status: ❌ Box is on wrong pallet

Wrong Relationship Type:

(:Keys)-[:CURRENT_IN]->(:Drawer)
(:Keys)-[:NORMAL_ON]->(:KeyHook)
// Status: ❌ Keys are contained but should be hanging

GraphQL Field Definitions

The spatial system exposes several fields in the GraphQL API with specific semantic meanings:

Location Fields Reference

Field Type Purpose Data Source Example
location String Current location (human-readable) spatialParent.name "Kitchen Drawer 2"
spatialParent SpatialRef Direct container object Neo4j (:obj)-[:IN]->(:container) {id: "123", name: "Kitchen Drawer 2", type: "Drawer"}
spatialChildren [ObjectInstance] Objects contained in this object Neo4j (:child)-[:IN]->(this) [{id: "456", name: "Fork", type: "Utensil"}]
spatialHierarchy [SpatialRef] Path from object to root container Neo4j parent relationship traversal [{name: "Fork"}, {name: "Drawer"}, {name: "Kitchen"}]
currentPlacement Placement Current location (structured) Neo4j CURRENT_* relationships {id: "123", name: "Kitchen Drawer 2", type: "Drawer", relationship: "CURRENT_IN"}
normalPlacement Placement Expected location (structured) Neo4j NORMAL_* relationships {id: "789", name: "Utensil Rack", type: "Rack", relationship: "NORMAL_ON"}
misplacementStatus MisplacementStatus Comparison of current vs normal Computed from current vs normal relationships {isCorrect: false, issue: "wrong_relationship", details: "Should be ON not IN"}
positionalRelationships [PositionalRelationship] Relative positioning relationships Neo4j [:LEFT_OF\|RIGHT_OF\|ABOVE\|UNDER\|ON\|ATTACHED_TO\|NEXT_TO] [{type: "LEFT_OF", relatedObject: {name: "Computer"}}]

Field Usage Guidelines

For Display:

For Operations:

For Navigation:

Implementation Details

Neo4j Relationships

// Current location (where object is now)
(:ObjectInstance {name: "Keys"})-[:CURRENT_IN]->(:ObjectInstance {name: "Kitchen Drawer"})

// Expected location (where object should be)
(:ObjectInstance {name: "Keys"})-[:NORMAL_ON]->(:ObjectInstance {name: "Key Hook"})

// Misplacement detection: Keys are IN drawer but should be ON hook
// Issue: Wrong relationship type and wrong target object

// Current positional relationships
(:ObjectInstance {name: "Lamp"})-[:CURRENT_LEFT_OF]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Mouse"})-[:CURRENT_RIGHT_OF]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Monitor"})-[:CURRENT_ABOVE]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Keyboard"})-[:CURRENT_UNDER]->(:ObjectInstance {name: "Monitor"})

// Normal/expected positional relationships
(:ObjectInstance {name: "Lamp"})-[:NORMAL_RIGHT_OF]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Mouse"})-[:NORMAL_LEFT_OF]->(:ObjectInstance {name: "Computer"})
// Result: Lamp and Mouse are on wrong sides - need to swap positions

// Current support and attachment relationships
(:ObjectInstance {name: "Laptop"})-[:CURRENT_ON]->(:ObjectInstance {name: "Desk"})
(:ObjectInstance {name: "Monitor"})-[:CURRENT_ATTACHED_TO]->(:ObjectInstance {name: "Arm"})

// Expected support relationships
(:ObjectInstance {name: "Laptop"})-[:NORMAL_IN]->(:ObjectInstance {name: "Storage_Case"})
// Result: Laptop should be stored, not on desk

// Current spatial hierarchy query (path to root via current parent relationships only)
MATCH path = (:ObjectInstance {id: $objectId})-[:CURRENT_IN|CURRENT_ON*0..]->(root:ObjectInstance)
WHERE NOT (root)-[:CURRENT_IN|CURRENT_ON]->()  // root has no current spatial parent
RETURN [node IN nodes(path) | {id: node.id, name: node.name, type: COALESCE(node.class, 'GenericObject')}] as hierarchy

// Normal/expected spatial hierarchy query
MATCH path = (:ObjectInstance {id: $objectId})-[:NORMAL_IN|NORMAL_ON*0..]->(root:ObjectInstance)
WHERE NOT (root)-[:NORMAL_IN|NORMAL_ON]->()  // root has no normal spatial parent
RETURN [node IN nodes(path) | {id: node.id, name: node.name, type: COALESCE(node.class, 'GenericObject')}] as hierarchy

// Misplacement detection query
MATCH (obj:ObjectInstance {id: $objectId})
OPTIONAL MATCH (obj)-[current:CURRENT_IN|CURRENT_ON|CURRENT_LEFT_OF|CURRENT_RIGHT_OF|CURRENT_ABOVE|CURRENT_UNDER|CURRENT_NEXT_TO|CURRENT_ATTACHED_TO]->(currentTarget:ObjectInstance)
OPTIONAL MATCH (obj)-[normal:NORMAL_IN|NORMAL_ON|NORMAL_LEFT_OF|NORMAL_RIGHT_OF|NORMAL_ABOVE|NORMAL_UNDER|NORMAL_NEXT_TO|NORMAL_ATTACHED_TO]->(normalTarget:ObjectInstance)
RETURN {
  objectId: obj.id,
  currentRelationship: type(current),
  currentTarget: {id: currentTarget.id, name: currentTarget.name},
  normalRelationship: type(normal),
  normalTarget: {id: normalTarget.id, name: normalTarget.name},
  isCorrectLocation: (type(current) = type(normal) AND currentTarget.id = normalTarget.id),
  isCorrectRelationship: (type(current) = type(normal)),
  isCorrectTarget: (currentTarget.id = normalTarget.id)
} as misplacementStatus

// Current positional relationships query
MATCH (obj:ObjectInstance {id: $objectId})
OPTIONAL MATCH (obj)-[r:CURRENT_LEFT_OF|CURRENT_RIGHT_OF|CURRENT_ABOVE|CURRENT_UNDER|CURRENT_ON|CURRENT_ATTACHED_TO|CURRENT_NEXT_TO]->(related:ObjectInstance)
RETURN {
  relationshipType: type(r),
  relatedObject: {id: related.id, name: related.name, type: COALESCE(related.class, 'GenericObject')},
  properties: properties(r)
} as currentRelationship

// Normal/expected positional relationships query
MATCH (obj:ObjectInstance {id: $objectId})
OPTIONAL MATCH (obj)-[r:NORMAL_LEFT_OF|NORMAL_RIGHT_OF|NORMAL_ABOVE|NORMAL_UNDER|NORMAL_ON|NORMAL_ATTACHED_TO|NORMAL_NEXT_TO]->(related:ObjectInstance)
RETURN {
  relationshipType: type(r),
  relatedObject: {id: related.id, name: related.name, type: COALESCE(related.class, 'GenericObject')},
  properties: properties(r)
} as normalRelationship

GraphQL Resolver Population

The getObjectDetailsComplete resolver populates spatial fields as follows:

# Current location fields (implemented)
"location": spatial_parent["name"] if spatial_parent else None,
"spatialParent": current_spatial_parent,  # From current parent relationships (CURRENT_IN, CURRENT_ON, CURRENT_CONTAINS)  
"currentPlacement": current_placement,  # Current location with relationship type
"normalPlacement": normal_placement,  # Expected location with relationship type
"spatialChildren": spatial_children,  # Objects with current parent relationships to this object
"spatialHierarchy": current_spatial_hierarchy,  # Current path to root container
"misplacementStatus": misplacement_status,  # Comparison of current vs normal location

# Current relationship fields
"currentPositionalRelationships": current_positional_relationships,  # Current CURRENT_* relationships

# Normal/expected relationship fields
"normalPositionalRelationships": normal_positional_relationships,  # Expected NORMAL_* relationships

Resolver Enforcement & Upsert Logic (2025-07-01)

The GraphQL resolvers are the single point that guarantees spatial consistency.

By co-locating this logic in the backend we avoid race-conditions where a client would have to call “remove + add” and briefly leave invalid graph states. Clients simply express intent; the resolver mutates the graph safely and atomically.

Use Cases

UC-305: Inventory Audit & Misplacement Detection

Scenario: Compare where objects are vs where they should be

  1. System queries both CURRENT_* and NORMAL_* relationships with full spatial detail
  2. Dashboard compares currentPlacement vs normalPlacement
  3. Flags mismatched objects for user attention
  4. User can either:
    • Accept new location (update SHOULD_BE_IN)
    • Move object to correct location
    • Add to move collection for bulk operations

Example:

// Object current location
(:Drill)-[:CURRENT_ON]->(:WorkBench)

// Object expected location  
(:Drill)-[:NORMAL_IN]->(:ToolRack)

// Detailed issue: Drill is ON workbench but should be IN tool rack

// Result: Drill is misplaced, flag for user attention

Spatial Navigation

Breadcrumb Navigation: Use spatialHierarchy to show location path (parent relationships only)

Container/Support Contents: Use spatialChildren to show what’s contained or supported

Positional Relationship Display: Show adjacent/positioned objects separately

Implementation Status

✅ Implemented

❌ TODO: Missing Implementation

For implementation details, see the System Architecture and Brand Requirements for UI specifications.