<!--
SPDX-FileCopyrightText: 2026 Jared Callaham <jared.callaham@gmail.com>

SPDX-License-Identifier: GPL-3.0-or-later
-->

# Data Model: Editable Block Labels in Parameter Panel

**Date**: 2026-01-16
**Feature**: 013-editable-block-labels
**Phase**: 1 (Design & Contracts)

## Overview

This feature does not introduce new entities or data structures. It adds a UI interaction for editing an existing entity attribute (Block.label). This document clarifies the existing data model and validation rules relevant to label editing.

---

## Entity: Block (Existing)

**Description**: Base class for all control system blocks. Represents a single block on the diagram canvas.

**Location**: `src/lynx/blocks/base.py` (lines 60-107)

### Attributes

| Attribute | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `id` | `str` | Yes | N/A | Unique identifier for the block (immutable) |
| `type` | `str` | Yes | N/A | Block type identifier (gain, transfer_function, state_space, sum, io_marker) |
| `position` | `Dict[str, float]` | Yes | `{"x": 0.0, "y": 0.0}` | Canvas position (x, y coordinates) |
| `label` | `str` | No | `id` value | User-facing name for the block (editable) |
| `label_visible` | `bool` | No | `False` | Whether label is displayed on canvas |
| `flipped` | `bool` | No | `False` | Horizontal flip for visual layout (Gain/TF blocks) |
| `custom_latex` | `Optional[str]` | No | `None` | Custom LaTeX override for block rendering |
| `width` | `Optional[float]` | No | `None` | Block width in pixels (uses block-type default if None) |
| `height` | `Optional[float]` | No | `None` | Block height in pixels (uses block-type default if None) |
| `_parameters` | `List[Parameter]` | N/A | `[]` | Block-specific parameters (internal) |
| `_ports` | `List[Port]` | N/A | `[]` | Input/output connection points (internal) |

### Label Attribute Details

**Attribute**: `label`

**Purpose**: User-facing name for a block, used for identification in UI and export operations (e.g., python-control subsystem extraction).

**Validation Rules** (enforced in `diagram.update_block_label()`):
- **FR-005**: Leading and trailing whitespace trimmed before persistence
- **FR-006**: Empty or whitespace-only labels revert to block `id`
- **FR-011**: Newlines and tabs normalized to spaces (frontend), control characters stripped (Python `.strip()`)
- **Uniqueness**: Labels NOT required to be unique (multiple blocks can have same label; blocks remain uniquely identified by `id`)

**Default Behavior**:
- Block constructor sets `self.label = label if label is not None else id` (line 99)
- If user never customizes label, it displays as block ID
- If user clears label in UI, it reverts to block ID (indistinguishable from never-customized state)

**Lifecycle**:
1. **Creation**: Block created with `label=None` → defaults to `id`
2. **First edit**: User sets label via canvas double-click or Parameter Panel → `label = "User Value"`
3. **Subsequent edits**: User can change label at any time via either editing method
4. **Clear/empty**: User clears label → `label = id` (revert to default)
5. **Persistence**: Label saved to JSON diagram file via Pydantic serialization

**Relationships**:
- **Independent from `label_visible`**: Label value stored regardless of visibility state (FR-007, FR-008)
- **Synced to frontend**: Changes propagate via anywidget traitlet sync (`diagram_state` traitlet)
- **Used in exports**: IOMarker labels used in python-control subsystem extraction (feature 012)

---

## Entity: Block Label Update Action (Existing)

**Description**: Action message sent from frontend to backend to update a block's label.

**Location**: `js/src/utils/traitletSync.ts` (sendAction) + `src/lynx/widget.py` (_handle_update_block_label)

### Action Schema

```typescript
// Frontend action dispatch
{
  type: "updateBlockLabel",
  payload: {
    blockId: string,      // Target block identifier (required)
    label: string         // New label value (required, may be empty string)
  },
  timestamp: number       // Action timestamp (auto-generated by sendAction)
}
```

### Python Handler

```python
# widget.py (lines 427-438) - EXISTING
def _handle_update_block_label(self, payload: Dict[str, Any]) -> None:
    """Handle updateBlockLabel action."""
    block_id = payload.get("blockId", "")
    new_label = payload.get("label", "")

    # Update label via diagram method (with undo support)
    if self.diagram.update_block_label(block_id, new_label):
        self._update_diagram_state()
```

### Validation Flow

```
Frontend (LabelEditor)
  ↓ (normalize whitespace, trim)
sendAction("updateBlockLabel", {blockId, label})
  ↓ (traitlet sync to Python)
widget._handle_update_block_label(payload)
  ↓
diagram.update_block_label(block_id, label)
  ↓ (validate: empty → id, save undo state)
block.label = label if label.strip() else block.id
  ↓
widget._update_diagram_state()
  ↓ (traitlet sync to frontend)
React Flow re-renders with new label
```

---

## State Transitions

### Block Label State Machine

```
┌─────────────────┐
│  Default State  │  label = id (never customized)
│   label = id    │
└────────┬────────┘
         │
         │ User edits label (non-empty after trim)
         ↓
┌─────────────────┐
│  Custom Label   │  label = "User Value"
│ label ≠ id      │
└────────┬────────┘
         │
         │ User clears label (empty after trim)
         ↓
┌─────────────────┐
│  Default State  │  label = id (reverted to default)
│   label = id    │  ← Indistinguishable from never-customized
└─────────────────┘
```

**State Invariants**:
- `label` is NEVER null (always a non-empty string)
- `label` is NEVER empty string (empty input reverts to `id`)
- `label` MAY equal `id` (either default or user explicitly sets label to match ID)
- `label` visibility state (`label_visible`) is independent from `label` value

---

## Validation Rules Summary

| Rule | Enforced By | Location | Spec Reference |
|------|-------------|----------|----------------|
| Trim whitespace | Frontend + Backend | LabelEditor.tsx, diagram.py:942 | FR-005 |
| Empty → revert to ID | Backend | diagram.py:942 | FR-006 |
| Normalize newlines/tabs | Frontend | LabelEditor.tsx (normalizeLabel) | FR-011 |
| Strip control chars | Backend | Python `.strip()` | FR-011 |
| Allow duplicates | Backend | No uniqueness check in diagram.py | Edge Cases line 64 |
| Accept Unicode | Both | TypeScript string + Python str | FR-011 |

---

## Data Persistence

**Format**: JSON (Pydantic serialization)

**Schema** (Block serialization in `blocks/base.py:to_dict()`):
```json
{
  "id": "block_123",
  "type": "gain",
  "position": {"x": 100.0, "y": 200.0},
  "label": "Controller Gain",
  "label_visible": true,
  "flipped": false,
  "custom_latex": null,
  "width": 120.0,
  "height": 80.0,
  "parameters": [
    {"name": "K", "value": 5.0, "expression": "5.0"}
  ],
  "ports": [
    {"id": "in", "type": "input"},
    {"id": "out", "type": "output"}
  ]
}
```

**Migration**: No migration needed (label attribute already exists, defaults handled by constructor).

---

## Data Access Patterns

### Read Operations
1. **Parameter Panel rendering**: Block object passed as prop, `block.label` accessed directly
2. **Canvas label display**: Block data in React Flow node, `data.label` used by EditableLabel
3. **Export operations**: IOMarker labels accessed for subsystem naming in python-control export

### Write Operations
1. **Canvas double-click**: EditableLabel → sendAction → widget → diagram.update_block_label
2. **Parameter Panel edit**: LabelEditor → sendAction → widget → diagram.update_block_label (SAME path)

**Write Consistency**: Both edit methods use identical backend code path, ensuring consistent validation and undo behavior.

---

## Performance Characteristics

| Operation | Complexity | Expected Latency |
|-----------|------------|------------------|
| Label read (frontend) | O(1) | <1ms (direct property access) |
| Label update (frontend→Python) | O(1) | <50ms (traitlet sync + dict update) |
| Label update (Python→frontend) | O(1) | <100ms (traitlet sync + React re-render) |
| Validation (empty check) | O(1) | <1ms (string length check) |
| Whitespace normalization | O(n) where n=label length | <1ms (regex replace + trim) |

**Scalability**: Label operations scale independently of diagram size (block count doesn't affect label update performance).

---

## Summary

- **No new entities**: Feature uses existing Block entity and label attribute
- **No schema changes**: All data structures already support label editing
- **Validation rules**: Documented for frontend (normalization) and backend (empty → ID)
- **State machine**: Three-state model (default, custom, reverted) with clear transitions
- **Performance**: O(1) operations meet <50ms persistence and <100ms update targets (SC-001, SC-002)

Ready for contracts generation (Phase 1 continuation).
