# MovePipelineItem

## Permission Scope

item

## Overview

Moves an item to a target stage on the same pipeline. Creates a PipelineStageTransition record for audit history. Moving an item never changes lifecycle to CLOSED on its own — closing is always explicit via ClosePipelineItem (or a domain-specific wrapper command).

**Note for domain-backed Items**: Items that back a domain row may carry additional invariants enforced by a domain-specific wrapper command. For example, a consuming module can wrap this command in its own domain command that server-stamps a domain field (such as a completion timestamp) on the first transition into a DONE-category stage. Calling `movePipelineItem` directly on such an item is permitted (e.g. for ops-side backfills), but it bypasses those domain invariants — production code paths must call the domain wrapper instead so the invariant is upheld. This module itself does not implement any such auto-stamp; it only records the PipelineStageTransition.

## Business Rules

- PipelineItem must exist
- PipelineItem must be in OPEN lifecycle (not DRAFT or CLOSED)
- Target stage must exist
- Target stage must be on the same pipeline as the item
- Target stage must be different from the current stage
- A PipelineStageTransition record is created for every move
- Moving to a DONE or CANCELED category stage does NOT change lifecycle; the item remains OPEN until an operator explicitly closes it
- An optional 1-based `position` can be supplied to insert the moved item at a specific slot in the target stage. The target stage is renormalized to a contiguous `1..N` sequence with the moved item at the chosen slot, so the drop lands correctly regardless of any gaps or duplicates in the stored positions. Zero or negative values are clamped to the top; values past the end are clamped to the end. When omitted, the item is appended to the end

## Process Flow

```mermaid
flowchart TD
    A[Receive movePipelineItem input] --> B{PipelineItem exists?}
    B -->|No| C[Return ITEM_NOT_FOUND]
    B -->|Yes| D{PipelineItem OPEN?}
    D -->|No| E[Return ITEM_NOT_OPEN]
    D -->|Yes| F{Target stage exists?}
    F -->|No| G[Return STAGE_NOT_FOUND]
    F -->|Yes| H{Target on same pipeline?}
    H -->|No| I[Return STAGE_NOT_ON_PIPELINE]
    H -->|Yes| J{Same as current stage?}
    J -->|Yes| K[Return SAME_STAGE]
    J -->|No| L[Move item to target stage]
    L --> M[Create PipelineStageTransition record]
    M --> N[Return moved PipelineItem]
```

## External Dependencies

- None

## Error Scenarios

- **ITEM_NOT_FOUND**: Specified item ID does not exist
- **ITEM_NOT_OPEN**: PipelineItem is not in OPEN lifecycle (it is DRAFT or CLOSED)
- **STAGE_NOT_FOUND**: Specified stage ID does not exist
- **STAGE_NOT_ON_PIPELINE**: Target stage is not on the same pipeline as the item
- **SAME_STAGE**: Target stage is the same as the item's current stage

## Test Cases

- moves item to a different stage on the same pipeline
- creates a PipelineStageTransition record
- keeps lifecycle OPEN when target category is DONE (no auto-close)
- keeps lifecycle OPEN when target category is CANCELED (no auto-close)
- keeps lifecycle OPEN when target category is NOT_STARTED or IN_PROGRESS
- appends to end when position is not provided
- inserts at the requested 1-based position when provided
- inserts at the chosen slot even when sibling positions are sparse (no top-snap)
- inserts at the chosen slot even when sibling positions are duplicated (no bottom-snap)
- clamps an out-of-range position to the end of the target stage
- clamps a zero or negative position to the top of the target stage
- returns error when item not found
- returns error when item is not OPEN (DRAFT)
- returns error when item is not OPEN (CLOSED)
- returns error when target stage not found
- returns error when target stage is on a different pipeline
- returns error when target stage is the same as current stage
