# Shift

## Description

Shift is a **planned work slot on a concrete date (予定の1コマ)** — the atom of the shift schedule, and nothing more than demand: when, where, and what kind of work is needed. It anchors a plan to a calendar `date`, belongs to exactly one `ShiftSchedule` period, optionally instantiates a `ShiftPattern`, and classifies the slot by `shiftType`: 通常 (NORMAL), 中抜け (SPLIT), 通し (THROUGH), 宿直 (ON_CALL), or 応援 (SUPPORT).

A Shift names **nobody**. "Who staffs this" is `ShiftPlacement` and only `ShiftPlacement`, so an **open shift** is not a shift with a null owner — it is a shift no ACTIVE placement references (issues #11, #6, ADR-017). Keeping the two definitions in one place is what stopped them drifting apart.

The planned working time is carried by an ordered `segments` array embedded directly on the Shift (ADR-022), not by a separate entity. A 通常 / 通し / 宿直 shift is a single segment; a 中抜け (SPLIT) shift is two or more segments separated by a real, unpaid gap (the worker leaves the site between them). This makes 中抜け first-class and symmetric with the actuals side — each planned segment maps to a time-tracking `ReportedTimeBlock` (`WORK`), and the gap between segments maps to a `STEP_OUT` block. The Shift itself keeps only the span **envelope** (`plannedStartAt` = earliest segment start, `plannedEndAt` = latest segment end) for shift-schedule-view queries.

Shift carries **no lifecycle of its own**. Whether a slot is committed to workers is its `ShiftSchedule`'s status: while the shift schedule is `DRAFT` the shift is freely editable, and confirming the shift schedule commits every slot in the period at once. The one thing a period-level status cannot express — withdrawing a single slot out of an otherwise confirmed table — is the shift's own `cancelledAt`, a recorded fact rather than a deletion. Actual worked time is not stored here either: the shift is the PLANNED side, compared against time-tracking `ReportedTimeBlock` / `CalculatedTimeBlock` to derive variance (遅刻 / 早退 / 未出勤 / 超過; ADR-014, ADR-017).

The planning intent that a slot was "meant for X" is not lost by dropping the home assignment: it is the first `ShiftPlacement` on the slot, and a later substitution leaves a `SUPERSEDED` row pointing at its replacement. The audit trail lives in one place instead of two that could disagree.

## Domain Model Definitions

### Model type

Standard

Shift became Standard when its lifecycle moved to `ShiftSchedule` (the publish axis) and `ShiftPlacement` (the staffing axis). `cancelledAt` is a recorded withdrawal, not a state machine.

### Command Definitions

- createShift — add a slot to a ShiftSchedule period for a date with one or more segments; optionally from a ShiftPattern (copying its segments)
- updateShift — adjust classification, site, or segments (add / reorder / update / remove segments in the embedded array) while the parent ShiftSchedule is `DRAFT`
- cancelShift — withdraw a single slot by stamping `cancelledAt`; works in a `CONFIRMED` period too

### Query Definitions

- getShift — single lookup by id, returns any status (segments included in response)
- listPublishedShifts — slots in CONFIRMED shift schedules for a date range / site (shift schedule view), paginated
- listOpenShifts — live slots in a CONFIRMED shift schedule that no ACTIVE placement staffs, paginated
- listShifts — unfiltered list across all statuses, paginated

### Models

- Shift

**Embedded: `segments` array**

Each element in the `segments` array represents a single contiguous planned work interval (formerly the independent `ShiftSegment` entity, embedded per ADR-022):

| Field | Type | Description |
|---|---|---|
| `sequence` | int | 1-based order within the Shift |
| `plannedStartAt` | datetime | Planned start of the interval |
| `plannedEndAt` | datetime | Planned end of the interval |
| `breakMinutes` | int | Intra-segment rest (non-negative, < gross span) |

### Invariants

- `shiftScheduleId` is required: every Shift belongs to exactly one ShiftSchedule period, and its `date` falls within that period's `[startDate, endDate]`
- The shift has no `status`; committed-ness is `ShiftSchedule.status`
- Editability is shift-schedule-scoped: a shift whose ShiftSchedule is `DRAFT` may be edited; once the ShiftSchedule is `CONFIRMED`, `updateShift` is rejected and a change means withdrawing the slot or adding a new one
- `cancelledAt` null = live, non-null = withdrawn; a withdrawn slot is immutable and accepts no placements, and the row is kept rather than deleted
- A shift has **at least one** segment in the `segments` array; the planned working time lives on the segments, not on the Shift
- `plannedStartAt` / `plannedEndAt` are the span **envelope**: they equal the earliest segment start and the latest segment end, and stay consistent with the segments (the envelope may cross midnight for 通し / 宿直, which is why the bounds are datetimes)
- `date` is the business date the slot belongs to (used with the time-tracking `CompanyHoliday` calendar for 所定日 / 休日 determination) and is consistent with the first segment's start
- `shiftType` uses normalized enum values — `NORMAL` (通常) / `SPLIT` (中抜け) / `THROUGH` (通し) / `ON_CALL` (宿直) / `SUPPORT` (応援) (ADR-006)
- Segment count is consistent with `shiftType`: `SPLIT` (中抜け) has two or more segments separated by a positive gap; `NORMAL` / `THROUGH` / `ON_CALL` have exactly one; `SUPPORT` may have one or more
- `segments[].sequence` is contiguous (1, 2, 3, …) and consistent with time order
- `segments[].plannedEndAt` is strictly after `segments[].plannedStartAt`; a single segment may cross midnight (通し / 宿直)
- Segments of the same Shift do not overlap; consecutive segments leave a **positive gap** — a zero gap means the two should be one segment, not a 中抜け
- The inter-segment gap is the 中抜け (step-out) interval — unpaid time the worker is off-site — and is distinct from `breakMinutes`
- `segments[].breakMinutes` is a non-negative minute count strictly less than the segment's gross span; it is an **intra-segment** rest (worker stays; 拘束時間 within one 勤務), not a 中抜け
- Break time is per-segment; the 中抜け itself is the *gap between* segments, not a break value on the Shift
- `siteId` (organization) is optional; a multi-site or 応援 slot carries the Site it is staffed at
- When `shiftPatternId` is set, the segments are instantiated from the pattern's `segments`, though the planned times may override the pattern
- A slot in a CONFIRMED shift schedule with no ACTIVE placement is a valid, advertisable open shift; it is "filled" only via a `ShiftPlacement`

### Relationships

- **References ShiftPattern** (optional): `shiftPatternId` instantiates a reusable pattern, copying its `segments` onto this date
- **Belongs to ShiftSchedule**: `shiftScheduleId` is the period this slot is planned in; the slot's committed-ness is that shift schedule's `status`
- **References Site** (organization, cross-module, optional): `siteId` places the slot at a facility for multi-site / 応援 placement
- **Reads CompanyHoliday** (time-tracking, cross-module): 所定日 / 休日 determination for `date` reads the holiday calendar (scheduling does not own holidays; ADR-017)
- **Referenced by ShiftPlacement**: placements (including 応援 / open-shift fills) reference this Shift
- **Variance against actuals**: each planned segment is compared with time-tracking `ReportedTimeBlock` / `CalculatedTimeBlock` (WORK / BREAK / STEP_OUT) to derive 遅刻 / 早退 / 未出勤 / 超過 (ADR-014); "Pay From Schedule" is left as future rule room (ADR-017)
