# AttachLabelToPipelineItem

## Permission Scope

itemLabel

## Overview

Attaches an existing label to an item. Both the label and the item must belong to the same pipeline. Re-attaching the same label is rejected — use DetachLabelFromPipelineItem first if the join row needs to be recreated.

## Business Rules

- PipelineItem must exist
- PipelineLabel must exist
- PipelineLabel must belong to the same pipeline as the item
- The pair (itemId, labelId) must not already exist
- `createdAt` records when the label was attached
- An [PipelineItemChange](../model/PipelineItemChange.md) row is appended (changeType=LABEL, newValue=labelId)
- Consumer extension fields (declared via the `fields` extension point) are passed through to the insert

## Process Flow

```mermaid
flowchart TD
    A[Receive attachLabelToPipelineItem input] --> B{PipelineItem exists?}
    B -->|No| C[Return ITEM_NOT_FOUND]
    B -->|Yes| D{PipelineLabel exists?}
    D -->|No| E[Return LABEL_NOT_FOUND]
    D -->|Yes| F{PipelineLabel.pipelineId == PipelineItem.pipelineId?}
    F -->|No| G[Return LABEL_NOT_ON_ITEM_PIPELINE]
    F -->|Yes| H{Pair already attached?}
    H -->|Yes| I[Return LABEL_ALREADY_ATTACHED]
    H -->|No| J[Persist PipelineItemLabel]
    J --> L[Append PipelineItemChange row - LABEL]
    L --> K[Return created PipelineItemLabel]
```

## External Dependencies

- None

## Error Scenarios

- **ITEM_NOT_FOUND**: Specified item ID does not exist
- **LABEL_NOT_FOUND**: Specified label ID does not exist
- **LABEL_NOT_ON_ITEM_PIPELINE**: PipelineLabel belongs to a different pipeline than the item
- **LABEL_ALREADY_ATTACHED**: The label is already attached to the item

## Test Cases

- attaches a label that belongs to the same pipeline as the item
- writes a PipelineItemChange for label attach
- returns error when item does not exist
- returns error when label does not exist
- returns error when label belongs to a different pipeline
- returns error when label is already attached
