# Story 2.7: Sequence Diagram Type Renderer

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

As a user,
I want to create and view sequence diagrams with actors, lifelines, and messages,
so that I can model interaction patterns between system components over time.

## Acceptance Criteria

1. **Given** I open or create a sequence diagram, **When** the canvas renders, **Then** actors display at the top as labeled boxes with dashed vertical lifelines extending downward, **And** activation bars (thin rectangles) appear on lifelines during active processing.

2. **Given** a sequence diagram has messages, **When** rendered, **Then** synchronous messages show as solid arrows between lifelines, **And** asynchronous messages show as open/dashed arrows, **And** return messages show as dashed arrows, **And** message labels appear above the arrow.

3. **Given** a sequence diagram has fragments, **When** rendered, **Then** alt/loop/opt fragments display as labeled rectangles enclosing message groups, **And** fragment headers show the guard condition.

## Tasks / Subtasks

- [x] Task 1: Create sequence constants and type registry (AC: #1, #2)
  - [x]1.1: Create `apps/web/src/modules/diagram/types/sequence/constants.ts` with `SEQ_SIZES`, `resolveSequenceNodeType()`, `resolveSequenceEdgeType()`, `getSeqNodeSize()`
  - [x]1.2: Define `SEQ_LAYOUT` constants: `PARTICIPANT_SPACING`, `MESSAGE_START_Y`, `MESSAGE_SPACING`, `LIFELINE_PADDING`

- [x] Task 2: Create SeqParticipantNode custom @xyflow/react component (AC: #1)
  - [x]2.1: Create `apps/web/src/modules/diagram/types/sequence/SeqParticipantNode.tsx` — labeled box at top with icon, dashed vertical lifeline extending downward (CSS `::after`), activation bar rendering from `data.activations`

- [x] Task 3: Create SeqFragmentNode custom @xyflow/react component (AC: #3)
  - [x]3.1: Create `apps/web/src/modules/diagram/types/sequence/SeqFragmentNode.tsx` — labeled rectangle with fragment type header (alt/loop/opt) and guard condition from `data.tag`

- [x] Task 4: Create SeqSyncEdge custom @xyflow/react component (AC: #2)
  - [x]4.1: Create `apps/web/src/modules/diagram/types/sequence/SeqSyncEdge.tsx` — solid arrow, message label above, horizontal path at computed Y from `data.order`

- [x] Task 5: Create SeqAsyncEdge custom @xyflow/react component (AC: #2)
  - [x]5.1: Create `apps/web/src/modules/diagram/types/sequence/SeqAsyncEdge.tsx` — dashed arrow with open arrowhead, message label above, horizontal path at computed Y from `data.order`

- [x] Task 6: Create SeqReturnEdge custom @xyflow/react component (AC: #2)
  - [x]6.1: Create `apps/web/src/modules/diagram/types/sequence/SeqReturnEdge.tsx` — dashed arrow (thinner), message label above, horizontal path at computed Y from `data.order`

- [x] Task 7: Create custom sequence layout function (AC: #1, #2, #3)
  - [x]7.1: Create `apps/web/src/modules/diagram/lib/sequence-layout.ts` with `computeSequenceLayout()` — custom layout that positions participants horizontally, computes message Y positions from edge order, sizes participant nodes to full lifeline height, positions fragment nodes around their message ranges
  - [x]7.2: Unit tests for `computeSequenceLayout()` in `sequence-layout.test.ts`

- [x] Task 8: Update graph converter for sequence type resolution (AC: #1, #2)
  - [x]8.1: Import `resolveSequenceNodeType`, `resolveSequenceEdgeType` from `../types/sequence/constants` in `graph-converter.ts`
  - [x]8.2: Add `sequence` diagramType and `seq:` prefix handling in `resolveFlowNodeType()`
  - [x]8.3: Add `sequence` diagramType handling in `resolveFlowEdgeType()`

- [x] Task 9: Register sequence types in DiagramCanvas (AC: #1, #2, #3)
  - [x]9.1: Import SeqParticipantNode, SeqFragmentNode, SeqSyncEdge, SeqAsyncEdge, SeqReturnEdge in `DiagramCanvas.tsx`
  - [x]9.2: Add sequence entries to `nodeTypes` and `edgeTypes` objects (OUTSIDE component)
  - [x]9.3: Add sequence arrow markers to `MarkerDefs` — filled arrow for sync, open arrow for async/return

- [x] Task 10: Integrate sequence layout in computeLayout (AC: #1, #2, #3)
  - [x]10.1: In `elk-layout.ts` `computeLayout()`, detect sequence diagrams (check for `seqParticipant` node types) and route to `computeSequenceLayout()` instead of ELK
  - [x]10.2: Sequence node size handling in `buildElkGraph` for fallback (if used) — `getSeqNodeSize()` for participant fixed dimensions

- [x] Task 11: Add sequence CSS styles (AC: #1, #2, #3)
  - [x]11.1: Add `.seq-participant`, `.seq-participant-box`, `.seq-lifeline`, `.seq-activation`, `.seq-fragment`, `.seq-fragment-header` styles to `globals.css`
  - [x]11.2: Use `--diagram-sequence` amber accent color for all sequence elements

- [x] Task 12: Tests (AC: all)
  - [x]12.1: Unit tests for sequence constants — `resolveSequenceNodeType` for participant/fragment, `resolveSequenceEdgeType` for sync/async/return
  - [x]12.2: Unit tests for graph converter sequence type mapping — node and edge types correctly resolved for `diagramType === "sequence"`
  - [x]12.3: Unit tests for sequence layout — participant positioning, message Y computation, fragment bounds
  - [x]12.4: Update "all 6 diagram types" test to assert `seq:participant` → `seqParticipant` (currently untested at line 147 of graph-converter.test.ts)
  - [x]12.5: All tests pass — no regressions

## Dev Notes

### Overview — What This Story Builds

This story adds the Sequence diagram type renderer: interaction diagrams with actors/participants, dashed lifelines, temporal message ordering, and fragment containers (alt/loop/opt). **Sequence diagrams are fundamentally different from all other diagram types** because they use time-ordered vertical positioning rather than graph-theoretic layout algorithms like ELK.

**This story builds:**
- 2 custom sequence node components (SeqParticipantNode with lifeline, SeqFragmentNode)
- 3 custom sequence edge components (SeqSyncEdge, SeqAsyncEdge, SeqReturnEdge)
- Sequence constants and type registry (2 node subtypes + 3 edge types)
- **Custom sequence layout function** (NOT ELK — time-ordered vertical positioning)
- Graph converter sequence type resolution
- Sequence CSS styles with amber `--diagram-sequence` accent
- Sequence arrow markers in MarkerDefs

**This story does NOT implement:**
- Other diagram types (Story 2.8)
- Smart Inspector for sequence field editing (future epic)
- Manual node repositioning (Story 2.9)
- Liveblocks/CRDT integration (Epic 4)
- AI-triggered mutations (Epic 3)
- Nested diagrams (Epic 7)

### Architecture Compliance

**MANDATORY patterns from Architecture Decision Document:**

1. **Unified Graph Data Model (Decision 1):** Sequence nodes use type-prefixed `type` field (`seq:participant`, `seq:fragment`). Existing DiagramNode fields are semantically reused: `label` = actor name, `tag` = metadata (guard condition for fragments), `icon` = actor emoji (override), `color` = custom accent override, `lifeline` = true for participant nodes. Edge types use lowercase strings: `sync`, `async`, `return`. NO new fields on DiagramNode or DiagramEdge needed.

2. **Component Structure:** Feature code in `~/modules/diagram/types/sequence/` — follows the BPMN (`types/bpmn/`), E-R (`types/er/`), Org Chart (`types/orgchart/`), Architecture (`types/architecture/`) pattern.

3. **@xyflow/react Custom Nodes:** All custom node components use `NodeProps` typing. The `nodeTypes` and `edgeTypes` objects MUST be defined OUTSIDE the component (performance critical — established in Stories 2.1-2.6).

4. **Custom Layout (NOT ELK):** Sequence diagrams DO NOT use the standard ELK layered algorithm. A dedicated `computeSequenceLayout()` function handles time-ordered vertical positioning. The `computeLayout()` function in `elk-layout.ts` must detect sequence diagrams and route to this custom layout.

5. **Lean JSON Data Model:** No x/y positions stored for sequence nodes. All positioning computed by the custom layout at render time.

6. **Type Prefixing Convention (Enforcement Rule #5):** `seq:` prefix on DiagramNode.type. Edge types are bare lowercase strings: `sync`, `async`, `return`.

### Sequence Diagram Data Model — Field Mapping

The existing DiagramNode and DiagramEdge fields satisfy sequence diagram needs:

```typescript
// seq:participant node
{
  id: "server",
  type: "seq:participant",
  label: "API Server",           // Actor name
  icon: "⚙️",                    // Actor icon (overridable)
  color: "#f59e0b",              // Custom accent (optional)
  lifeline: true,                // Marks as lifeline participant
}

// seq:fragment node
{
  id: "alt-1",
  type: "seq:fragment",
  label: "alt",                  // Fragment type: "alt" | "loop" | "opt"
  tag: "[valid credentials]",    // Guard condition
  group: "m2,m3,m4",            // Comma-separated message IDs this fragment encompasses
}
```

**Edge model:**
```typescript
// Synchronous message
{
  id: "m1",
  from: "client",
  to: "server",
  label: "POST /login",          // Message label shown above arrow
  type: "sync",                  // "sync" = solid arrow
}

// Asynchronous message
{
  id: "m2",
  from: "server",
  to: "worker",
  label: "enqueue job",
  type: "async",                 // "async" = dashed open arrow
}

// Return message
{
  id: "m3",
  from: "server",
  to: "client",
  label: "200 OK",
  type: "return",                // "return" = dashed arrow
}
```

**Message ordering:** Messages are ordered by their position in the `edges` array. The layout function uses array index as temporal order. Edge `data.order` is set by the layout function at runtime.

### Sequence Node Types — Size Constants

```typescript
export const SEQ_SIZES = {
  participant: { w: 160, h: 60 },     // Top box dimensions (lifeline extends below)
  fragment:    { w: 0, h: 0 },        // Computed dynamically from contained messages
} as const;

export const SEQ_LAYOUT = {
  participantSpacing: 200,             // Horizontal spacing between participants
  messageStartY: 100,                  // Y offset for first message below participant box
  messageSpacing: 50,                  // Vertical spacing between messages
  lifelinePadding: 40,                // Extra space below last message
  activationWidth: 12,                 // Width of activation bar rectangles
  fragmentPadding: 16,                // Padding inside fragment rectangles
} as const;
```

### Sequence Node Type → @xyflow/react Type Mapping

| DiagramNode.type | @xyflow Node type | Component | Visual |
|---|---|---|---|
| `seq:participant` | `seqParticipant` | `SeqParticipantNode` | Labeled box + dashed vertical lifeline + activation bars |
| `seq:fragment` | `seqFragment` | `SeqFragmentNode` | Labeled rectangle with type header and guard condition |

**Type resolution:**
```typescript
export function resolveSequenceNodeType(type: string): string {
  const bare = type.startsWith("seq:") ? type.slice(4) : type;
  switch (bare) {
    case "participant":
      return "seqParticipant";
    case "fragment":
      return "seqFragment";
    default:
      return "seqParticipant"; // Default to participant
  }
}

export function resolveSequenceEdgeType(type: string | undefined): string {
  switch (type) {
    case "sync":
      return "seqSync";
    case "async":
      return "seqAsync";
    case "return":
      return "seqReturn";
    default:
      return "seqSync"; // Default to synchronous
  }
}
```

### Sequence Edge Types — Messages

| DiagramEdge.type | @xyflow Edge type | Visual |
|---|---|---|
| `sync` (or default) | `seqSync` | Solid line with filled arrowhead, label above |
| `async` | `seqAsync` | Dashed line with open arrowhead, label above |
| `return` | `seqReturn` | Dashed thin line with filled arrowhead, label above |

### Custom Sequence Layout — CRITICAL DIFFERENCE

**Sequence diagrams CANNOT use ELK.** ELK's layered/Sugiyama algorithm optimizes for graph-theoretic properties (crossing minimization, layer assignment). Sequence diagrams require **temporal ordering** — messages appear top-to-bottom in the order they occur, not based on graph structure.

**Layout Algorithm (`computeSequenceLayout`):**

```typescript
export function computeSequenceLayout(
  nodes: Node[],
  edges: Edge[],
): { nodes: Node[]; edges: Edge[] } {
  const participants = nodes.filter(n =>
    n.type === "seqParticipant"
  );
  const fragments = nodes.filter(n =>
    n.type === "seqFragment"
  );

  // 1. Position participants horizontally
  participants.forEach((p, i) => {
    p.position = { x: i * SEQ_LAYOUT.participantSpacing, y: 0 };
  });

  // 2. Compute lifeline height from message count
  const lifelineHeight =
    SEQ_LAYOUT.messageStartY +
    edges.length * SEQ_LAYOUT.messageSpacing +
    SEQ_LAYOUT.lifelinePadding;

  // 3. Set participant node height to cover full lifeline
  participants.forEach(p => {
    p.style = { ...p.style, height: lifelineHeight };
    (p.data as Record<string, unknown>).lifelineHeight = lifelineHeight;
  });

  // 4. Compute message Y positions and store in edge data
  const enrichedEdges = edges.map((edge, i) => ({
    ...edge,
    data: {
      ...edge.data,
      order: i,
      messageY: SEQ_LAYOUT.messageStartY + i * SEQ_LAYOUT.messageSpacing,
    },
  }));

  // 5. Compute activation bars per participant
  // Activation: starts at sync message received, ends at return sent
  // Store in participant data for rendering

  // 6. Position fragment nodes around their messages
  const participantXMap = new Map(
    participants.map(p => [p.id, p.position.x])
  );
  fragments.forEach(frag => {
    const fragData = frag.data as Record<string, unknown>;
    const messageIds = ((fragData.group as string) || "").split(",");
    const messageIndices = messageIds
      .map(id => enrichedEdges.findIndex(e => e.id === id))
      .filter(i => i >= 0);

    if (messageIndices.length > 0) {
      const minIdx = Math.min(...messageIndices);
      const maxIdx = Math.max(...messageIndices);
      const topY =
        SEQ_LAYOUT.messageStartY +
        minIdx * SEQ_LAYOUT.messageSpacing -
        SEQ_LAYOUT.fragmentPadding;
      const bottomY =
        SEQ_LAYOUT.messageStartY +
        maxIdx * SEQ_LAYOUT.messageSpacing +
        SEQ_LAYOUT.fragmentPadding;

      // Find leftmost and rightmost participants involved
      const involvedEdges = messageIndices.map(i => enrichedEdges[i]!);
      const allParticipantIds = new Set(
        involvedEdges.flatMap(e => [e.source, e.target])
      );
      const xPositions = [...allParticipantIds]
        .map(id => participantXMap.get(id) ?? 0);
      const minX = Math.min(...xPositions) - SEQ_LAYOUT.fragmentPadding;
      const maxX = Math.max(...xPositions) +
        SEQ_SIZES.participant.w +
        SEQ_LAYOUT.fragmentPadding;

      frag.position = { x: minX, y: topY };
      frag.style = {
        width: maxX - minX,
        height: bottomY - topY,
      };
    }
  });

  return {
    nodes: [...participants, ...fragments],
    edges: enrichedEdges,
  };
}
```

**Integration with `computeLayout` in `elk-layout.ts`:**
```typescript
// In computeLayout(), before ELK dispatch:
const isSequence = nodes.some(n => n.type === "seqParticipant");
if (isSequence) {
  // Sequence uses custom layout, not ELK worker
  const result = computeSequenceLayout(nodes, edges);
  resolve(result.nodes);
  // Note: edges are also enriched with data.order/messageY
  return;
}
```

**Important:** `computeSequenceLayout` is synchronous (no Web Worker needed). Sequence diagrams are always small (tens of participants, not hundreds of nodes), so the computation is instant.

### Sequence Edge Components — Custom Y-Positioned Rendering

All three edge components share a similar pattern: they compute their Y position from `data.order` and draw a horizontal arrow:

```typescript
import { EdgeLabelRenderer } from "@xyflow/react";
import type { EdgeProps } from "@xyflow/react";
import { SEQ_LAYOUT } from "./constants";

export function SeqSyncEdge(props: EdgeProps) {
  const edgeData = props.data as Record<string, unknown> | undefined;
  const order = (edgeData?.order as number) ?? 0;
  const messageY = (edgeData?.messageY as number) ??
    SEQ_LAYOUT.messageStartY + order * SEQ_LAYOUT.messageSpacing;

  // Horizontal arrow from source to target at messageY
  const isLeftToRight = props.sourceX < props.targetX;
  const startX = isLeftToRight
    ? props.sourceX + SEQ_SIZES.participant.w / 2
    : props.sourceX - SEQ_SIZES.participant.w / 2;
  const endX = isLeftToRight
    ? props.targetX - SEQ_SIZES.participant.w / 2
    : props.targetX + SEQ_SIZES.participant.w / 2;

  const path = `M ${startX} ${messageY} L ${endX} ${messageY}`;

  return (
    <>
      <path
        d={path}
        stroke="var(--diagram-sequence)"
        strokeWidth={1.5}
        fill="none"
        markerEnd="url(#seq-arrow-filled)"
      />
      {props.label && (
        <EdgeLabelRenderer>
          <div
            className="seq-edge-label"
            style={{
              transform: `translate(-50%, -100%) translate(${(startX + endX) / 2}px, ${messageY - 4}px)`,
              position: "absolute",
            }}
          >
            {props.label}
          </div>
        </EdgeLabelRenderer>
      )}
    </>
  );
}
```

**Differences between edge types:**
- `SeqSyncEdge`: solid stroke, filled arrowhead (`#seq-arrow-filled`)
- `SeqAsyncEdge`: dashed stroke (`strokeDasharray: "6 3"`), open arrowhead (`#seq-arrow-open`)
- `SeqReturnEdge`: dashed stroke (`strokeDasharray: "4 4"`), filled arrowhead, thinner stroke (1px)

### Participant Node Component — Lifeline + Activation Bars

```typescript
import { Handle, Position } from "@xyflow/react";
import type { NodeProps } from "@xyflow/react";
import type { DiagramNode } from "../graph";
import { HIDDEN_HANDLE } from "../architecture/constants";

export function SeqParticipantNode({ data }: NodeProps) {
  const d = data as unknown as DiagramNode & {
    label: string;
    lifelineHeight?: number;
    activations?: { y: number; height: number }[];
  };
  const icon = d.icon || "👤";
  const lifelineHeight = d.lifelineHeight ?? 400;
  const activations = d.activations ?? [];

  return (
    <div className="seq-participant" style={{ height: lifelineHeight }}>
      {/* Actor box at top */}
      <div className="seq-participant-box">
        <span className="seq-participant-icon">{icon}</span>
        <span className="seq-participant-label">{d.label}</span>
      </div>
      {/* Dashed lifeline */}
      <div
        className="seq-lifeline"
        style={{ height: lifelineHeight - SEQ_SIZES.participant.h }}
      />
      {/* Activation bars */}
      {activations.map((act, i) => (
        <div
          key={i}
          className="seq-activation"
          style={{ top: act.y, height: act.height }}
        />
      ))}
      <Handle type="target" position={Position.Top} style={HIDDEN_HANDLE} />
      <Handle type="source" position={Position.Bottom} style={HIDDEN_HANDLE} />
      <Handle type="target" position={Position.Left} id="left" style={HIDDEN_HANDLE} />
      <Handle type="source" position={Position.Right} id="right" style={HIDDEN_HANDLE} />
    </div>
  );
}
```

**Reuses `HIDDEN_HANDLE`** from architecture/constants.ts (extracted in Story 2.6 code review).

### Fragment Node Component

```typescript
import type { NodeProps } from "@xyflow/react";
import type { DiagramNode } from "../graph";

export function SeqFragmentNode({ data }: NodeProps) {
  const d = data as unknown as DiagramNode & { label: string };
  const fragmentType = d.label; // "alt" | "loop" | "opt"
  const guardCondition = d.tag; // "[condition]"

  return (
    <div className="seq-fragment">
      <div className="seq-fragment-header">
        <span className="seq-fragment-type">{fragmentType}</span>
        {guardCondition && (
          <span className="seq-fragment-guard">{guardCondition}</span>
        )}
      </div>
    </div>
  );
}
```

**Note:** Fragment nodes do NOT need handles — they are container nodes, not edge endpoints. They should be in the CONTAINER_TYPES set for BFS highlighting skip.

### Graph Converter Updates

Add sequence type resolution alongside existing types:

```typescript
// In resolveFlowNodeType():
if (diagramType === "sequence" || nodeType.startsWith("seq:")) {
  return resolveSequenceNodeType(nodeType);
}

// In resolveFlowEdgeType():
if (diagramType === "sequence") {
  return resolveSequenceEdgeType(edgeType);
}
```

**Import from sequence/constants.ts** — same pattern as BPMN, E-R, Org Chart, Architecture.

**graphToFlow for sequence diagrams:** Uses the standard default path (flat node mapping). No container nodes needed in the converter — fragments are regular nodes.

**CONTAINER_TYPES update:** Add `"seqFragment"` to the set so BFS highlighting skips fragment container nodes.

### DiagramCanvas Updates

```typescript
import { SeqParticipantNode } from "../../types/sequence/SeqParticipantNode";
import { SeqFragmentNode } from "../../types/sequence/SeqFragmentNode";
import { SeqSyncEdge } from "../../types/sequence/SeqSyncEdge";
import { SeqAsyncEdge } from "../../types/sequence/SeqAsyncEdge";
import { SeqReturnEdge } from "../../types/sequence/SeqReturnEdge";

const nodeTypes = {
  // Existing BPMN + E-R + Org Chart + Architecture types...
  seqParticipant: SeqParticipantNode,
  seqFragment: SeqFragmentNode,
};

const edgeTypes = {
  // Existing types...
  seqSync: SeqSyncEdge,
  seqAsync: SeqAsyncEdge,
  seqReturn: SeqReturnEdge,
};

const CONTAINER_TYPES = new Set([
  "bpmnPool", "bpmnLane", "bpmnGroup",
  "seqFragment",  // ← ADD
]);
```

**Arrow markers needed in MarkerDefs:**
```tsx
{/* Sequence markers */}
<marker
  id="seq-arrow-filled"
  viewBox="0 0 10 10"
  refX={10}
  refY={5}
  markerWidth={8}
  markerHeight={8}
  orient="auto-start-reverse"
>
  <path
    d="M 0 0 L 10 5 L 0 10 Z"
    fill="var(--diagram-sequence, #f59e0b)"
  />
</marker>
<marker
  id="seq-arrow-open"
  viewBox="0 0 10 10"
  refX={10}
  refY={5}
  markerWidth={8}
  markerHeight={8}
  orient="auto-start-reverse"
>
  <path
    d="M 0 0 L 10 5 L 0 10"
    fill="none"
    stroke="var(--diagram-sequence, #f59e0b)"
    strokeWidth={1.5}
  />
</marker>
```

### CSS Styles for Sequence Diagram

Add to `globals.css`. The sequence theme uses `--diagram-sequence` (amber accent: `oklch(0.795 0.184 86)`, already defined).

```css
/* ── Sequence Diagram Styles ────────────────────────────────────── */

.seq-participant {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 160px;
}

.seq-participant-box {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--node-bg);
  border: 1.5px solid var(--diagram-sequence);
  border-radius: 6px;
  padding: 8px 14px;
  width: 100%;
  cursor: pointer;
  z-index: 1;
}
.seq-participant-box:hover {
  background: color-mix(in oklch, var(--diagram-sequence) 8%, var(--node-bg));
}

.seq-participant-icon {
  font-size: 18px;
  flex-shrink: 0;
}

.seq-participant-label {
  font-weight: 600;
  font-size: 12px;
  color: var(--foreground);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.seq-lifeline {
  width: 0;
  border-left: 2px dashed var(--diagram-sequence);
  opacity: 0.5;
  position: absolute;
  top: 60px; /* Below participant box */
  left: 50%;
  transform: translateX(-50%);
}

.seq-activation {
  position: absolute;
  width: 12px;
  left: 50%;
  transform: translateX(-50%);
  background: color-mix(in oklch, var(--diagram-sequence) 20%, var(--node-bg));
  border: 1.5px solid var(--diagram-sequence);
  border-radius: 2px;
  z-index: 1;
}

.seq-fragment {
  background: color-mix(in oklch, var(--diagram-sequence) 4%, transparent);
  border: 1.5px solid var(--diagram-sequence);
  border-radius: 4px;
  width: 100%;
  height: 100%;
  position: relative;
}

.seq-fragment-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  border-bottom: 1px solid var(--diagram-sequence);
  border-right: 1px solid var(--diagram-sequence);
  border-bottom-right-radius: 8px;
  width: fit-content;
}

.seq-fragment-type {
  font-weight: 700;
  font-size: 11px;
  color: var(--diagram-sequence);
  text-transform: uppercase;
  font-family: monospace;
}

.seq-fragment-guard {
  font-size: 11px;
  color: var(--muted-foreground);
  font-family: monospace;
}

.seq-edge-label {
  font-size: 11px;
  color: var(--diagram-sequence);
  background: var(--node-bg);
  padding: 1px 6px;
  border-radius: 4px;
  font-family: monospace;
  pointer-events: none;
  white-space: nowrap;
}
```

### Existing Code to Reuse / Modify

| File | Action | What |
|------|--------|------|
| `apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx` | **MODIFY** | Register sequence node types, edge types, arrow markers, add `seqFragment` to CONTAINER_TYPES |
| `apps/web/src/modules/diagram/lib/graph-converter.ts` | **MODIFY** | Add sequence type resolution for nodes and edges |
| `apps/web/src/modules/diagram/lib/elk-layout.ts` | **MODIFY** | Add sequence diagram detection → route to custom layout; add `getSeqNodeSize()` handling |
| `apps/web/src/assets/styles/globals.css` | **MODIFY** | Add sequence CSS styles |
| `apps/web/src/modules/diagram/types/graph.ts` | **READ** | DiagramNode (lifeline, tag, icon, color, group) — already defined, no changes |
| `apps/web/src/modules/diagram/lib/bfs-path.ts` | **REUSE** | Path highlighting works for sequence — diagram-type-agnostic |
| `apps/web/src/modules/diagram/stores/useGraphStore.ts` | **REUSE** | highlightedNodeId — no changes needed |
| `apps/web/src/modules/diagram/hooks/useAutoLayout.ts` | **REUSE** | Auto-layout triggers computeLayout → routes to sequence layout |
| `apps/web/src/modules/diagram/types/architecture/constants.ts` | **REUSE** | `HIDDEN_HANDLE` constant for handle styles |

### Library & Framework Requirements

**No new packages required.** Everything built with existing dependencies:
- `@xyflow/react` 12.10.1 — custom nodes, edges, handles, EdgeLabelRenderer
- `elkjs` 0.11.0 — NOT used for sequence layout, but still used for other diagram types
- `zustand` 5.0.8 — highlight state (reuse existing)

### File Structure for This Story

New files:
```
apps/web/src/modules/diagram/
├── types/sequence/
│   ├── constants.ts                     # SEQ_SIZES, SEQ_LAYOUT, type resolution functions
│   ├── constants.test.ts                # Tests for sequence constants
│   ├── SeqParticipantNode.tsx           # Participant + lifeline + activation bars
│   ├── SeqFragmentNode.tsx              # Alt/loop/opt fragment container
│   ├── SeqSyncEdge.tsx                  # Synchronous message (solid arrow)
│   ├── SeqAsyncEdge.tsx                 # Asynchronous message (dashed open arrow)
│   └── SeqReturnEdge.tsx                # Return message (dashed arrow)
├── lib/
│   ├── sequence-layout.ts              # Custom sequence layout function
│   └── sequence-layout.test.ts         # Tests for sequence layout
```

Modified files:
```
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx  # Register sequence types + markers + CONTAINER_TYPES
apps/web/src/modules/diagram/lib/graph-converter.ts               # Sequence type mapping
apps/web/src/modules/diagram/lib/graph-converter.test.ts          # Add sequence converter tests + fix line 147
apps/web/src/modules/diagram/lib/elk-layout.ts                    # Sequence detection → custom layout routing
apps/web/src/modules/diagram/lib/elk-layout.test.ts               # Add sequence layout integration tests
apps/web/src/assets/styles/globals.css                             # Sequence CSS styles
```

### Project Structure Notes

- Sequence node components go in `~/modules/diagram/types/sequence/` — follows BPMN/E-R/Org Chart/Architecture pattern
- Custom sequence layout in `~/modules/diagram/lib/sequence-layout.ts` — separate module from `elk-layout.ts` because it uses a fundamentally different algorithm
- Tests co-located next to source files
- No barrel files — import from specific subpaths

### Anti-Patterns to Avoid

- **NEVER use ELK for sequence diagram layout** — sequence diagrams require temporal vertical ordering, not graph-theoretic layout. This is the single biggest mistake a developer could make on this story.
- **NEVER put `nodeTypes` or `edgeTypes` inside the component** — causes re-renders (established pattern)
- **NEVER hardcode positions for sequence nodes in GraphData** — all positioning from custom layout
- **NEVER import from `reactflow`** — use `@xyflow/react` (v12+)
- **NEVER use `require()`** — ESM-only project
- **NEVER co-locate feature code in route directories** — use `~/modules/diagram/`
- **NEVER store layout-computed positions in persisted graph data** — positions are ephemeral
- **NEVER add new fields to DiagramNode or DiagramEdge** — use existing fields (`tag`, `icon`, `color`, `group`, `lifeline`) with sequence semantics
- **NEVER create inline style objects in render** — use CSS classes; conditionally apply inline styles only when data-driven (e.g., activation bar positions)
- **NEVER use `??` for empty string fallbacks** — use `||` so falsy `""` falls back correctly (lesson from Story 2.5 code review)
- **NEVER create barrel `index.ts` files** — per project rules, no barrel files in feature modules
- **DO NOT implement Smart Inspector for sequence** — future story
- **DO NOT implement other diagram type renderers** — Story 2.8 handles flowcharts
- **DO NOT break existing tests** — 443 tests must continue passing (27 test files across monorepo)
- **DO NOT add arrow markers inline** — use SVG `<marker>` in `MarkerDefs` with `markerEnd="url(#seq-arrow-filled)"` / `url(#seq-arrow-open)`

### Previous Story Intelligence (Story 2.6 — Architecture)

**Key learnings to carry forward:**
- Constants file pattern: `ARCH_SIZES` → `SEQ_SIZES` equivalent. Type resolution functions: `resolveXNodeType()`, `resolveXEdgeType()`
- Node component pattern: Cast `data` as `DiagramNode & { label: string }`, use `Handle` with `style={HIDDEN_HANDLE}` (constant from architecture/constants.ts — reuse it)
- Edge component pattern: `BaseEdge` not used for sequence — custom SVG path for horizontal arrows at computed Y positions
- Graph converter: `resolveFlowNodeType` switch on `diagramType`, import type resolver from `types/[type]/constants`
- DiagramCanvas: import components, add to `nodeTypes`/`edgeTypes` objects OUTSIDE component
- CSS: Use `--diagram-[type]` CSS variable for accent colors. Use `color-mix()` for hover/bg tints
- `graphNodeToFlowNode` already spreads all DiagramNode fields into `data` — custom nodes access `data.tag`, `data.icon`, `data.lifeline` directly
- `flowNodeToGraphNode` already preserves `tag`, `icon`, `color`, `lifeline`, `group` fields — roundtrip works
- Code review lessons from 2.5/2.6: Remove dead helper functions, use `||` not `??` for empty string color guards, don't create inline style objects on every render, reuse `HIDDEN_HANDLE` constant
- `CONTAINER_TYPES` set needs to include `"seqFragment"` for BFS highlighting to skip fragments
- 443 tests currently pass across monorepo — don't break them

### Previous Story Intelligence (Story 2.3 — BPMN)

**Key learnings relevant to sequence:**
- BPMN uses compound ELK layout with pools/lanes — sequence does NOT. But BPMN's compound detection pattern in `computeLayout()` is the template for sequence detection routing
- BPMN's `MarkerDefs` pattern: `#bpmn-arrow-filled` and `#bpmn-arrow-open` — reuse same SVG marker structure for `#seq-arrow-filled` and `#seq-arrow-open`
- Multiple edge types per diagram (BPMN has sequence/message/association, sequence has sync/async/return) — each gets its own custom edge component
- The `flowToBpmnGraphData()` pattern shows how to reconstruct GraphData from @xyflow nodes for custom layout — sequence might need a similar helper

### Git Intelligence

Recent commits:
- `0a7838a feat: implement Story 2.3 — BPMN diagram type renderer`
- `7dd5af1 feat: implement Story 2.2 — ELK.js auto-layout engine in Web Worker`
- `5033109 feat: implement Story 2.1 — canvas workspace with @xyflow/react and unified graph model`

Established patterns:
- Commit message: `feat: implement Story X.Y — description`
- Feature code in `apps/web/src/modules/diagram/`
- Co-located tests next to source files
- `diagramTypeConfig` in DiagramCard.tsx already has sequence config: `{ label: "Sequence", icon: Icons.ArrowRightLeft, color: "text-amber-500" }`
- CSS variable `--diagram-sequence: oklch(0.795 0.184 86)` already defined in globals.css
- Graph-converter.test.ts line 129 already uses `seq:participant` but has no assertion (nodes[4] untested) — must update

### Latest Tech Information

**@xyflow/react 12.10.1 — Custom Edge SVG Paths:**
- Edge components render inside the `<svg>` layer of the ReactFlow pane
- Custom paths can be drawn directly with `<path>` elements using the same coordinate system as node positions
- `EdgeLabelRenderer` renders HTML labels in a separate div layer on top of the SVG — use `position: absolute` + `transform` for positioning
- Edge components receive `sourceX, sourceY, targetX, targetY` from handle positions, but custom implementations can ignore these and compute their own coordinates
- `markerEnd` prop on `<path>` references SVG markers defined in `<defs>` — same pattern as BPMN and architecture

**Edge Viewport Culling Consideration:**
- @xyflow determines edge visibility based on computed edge bounds from source/target handle positions
- If custom edges render far from handle positions (sequence messages at different Y), they may be culled from rendering
- Mitigation: Size participant nodes to cover the full lifeline height so handles are within the relevant viewport area
- Alternative: Use `data.order` to set participant node height covering all message positions

**CSS Shapes for Sequence Elements:**
- Lifeline: `border-left: 2px dashed` on a div positioned below the participant box
- Activation bar: Small rectangle (`width: 12px`) positioned absolutely on the lifeline
- Fragment: `border: 1.5px solid` rectangle with a header corner tab (border-bottom-right-radius on header div)
- All use `color-mix()` for tinted backgrounds and `cursor: pointer` for interactivity

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.7] — Full AC: actors, lifelines, activation bars, sync/async/return messages, alt/loop/opt fragments
- [Source: _bmad-output/planning-artifacts/epics.md#Technical Notes] — Custom layout logic needed, NOT standard ELK; custom nodes at types/sequence/; amber accent
- [Source: _bmad-output/planning-artifacts/architecture.md#Decision 1] — Unified Graph Data Model: `seq:` prefix, shared base fields, lifeline boolean
- [Source: _bmad-output/planning-artifacts/architecture.md#Enforcement Guidelines] — 7 mandatory rules, type prefixing: `seq:`
- [Source: _bmad-output/planning-artifacts/architecture.md#DiagramNode] — lifeline?: boolean for sequence participant nodes
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Diagram Type Renderers] — Sequence: Actor/Lifeline, Activation bar, Fragment (alt/loop/opt)
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Diagram Type Color Accents] — Sequence: amber oklch(0.795 0.184 86)
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#DiagramEdgeRenderer] — Sequence edges: sync/async/return
- [Source: _bmad-output/implementation-artifacts/2-6-architecture-diagram-type-renderer.md] — Architecture patterns: constants, nodes, edges, converter, canvas registration, HIDDEN_HANDLE, code review learnings
- [Source: apps/web/src/modules/diagram/types/graph.ts] — DiagramNode (lifeline, tag, icon, color, group), DiagramType includes "sequence"
- [Source: apps/web/src/modules/diagram/lib/graph-converter.ts] — Current converter with BPMN + E-R + OrgChart + Architecture resolution; line 43 comment "Future: sequence, flowchart"
- [Source: apps/web/src/modules/diagram/lib/graph-converter.test.ts] — Line 129 uses seq:participant, line 147 has no assertion for it (BUG to fix)
- [Source: apps/web/src/modules/diagram/lib/elk-layout.ts] — computeLayout() with compound BPMN detection pattern to follow for sequence detection
- [Source: apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx] — Canvas to register sequence types + markers + CONTAINER_TYPES
- [Source: apps/web/src/modules/diagram/components/DiagramCard.tsx] — Line 59: sequence config already defined with Icons.ArrowRightLeft
- [Source: apps/web/src/assets/styles/globals.css] — Line 28: `--diagram-sequence: oklch(0.795 0.184 86)` already defined

## Dev Agent Record

### Agent Model Used

Claude Opus 4.6 (1M context)

### Debug Log References

No issues encountered. All tests passed on first run.

### Completion Notes List

- Created sequence constants with SEQ_SIZES, SEQ_LAYOUT, type resolution functions, and getSeqNodeSize
- Implemented SeqParticipantNode with labeled box, dashed lifeline (CSS ::after equivalent via absolute div), and activation bar rendering
- Implemented SeqFragmentNode with fragment type header (alt/loop/opt) and guard condition display
- Implemented 3 custom edge components (SeqSyncEdge, SeqAsyncEdge, SeqReturnEdge) with horizontal arrow paths at computed Y positions, each with distinct visual styles (solid/dashed/thin-dashed)
- Created custom `computeSequenceLayout()` — synchronous time-ordered layout that positions participants horizontally, computes message Y from edge order, sizes lifelines, and positions fragments around their message ranges
- Updated graph-converter.ts with sequence type resolution (node: seq: prefix, edge: sync/async/return)
- Registered all sequence node/edge types in DiagramCanvas, added seqFragment to CONTAINER_TYPES for BFS skip
- Added seq-arrow-filled and seq-arrow-open SVG markers to MarkerDefs
- Integrated sequence detection in computeLayout() — routes to custom layout instead of ELK worker
- Added getSeqNodeSize fallback handling in buildElkGraph
- Added sequence CSS styles with amber accent (--diagram-sequence) following established pattern
- Fixed untested seq:participant assertion at line 147 of graph-converter.test.ts
- All 469 tests pass across monorepo (0 regressions)

### File List

**New files:**
- apps/web/src/modules/diagram/types/sequence/constants.ts
- apps/web/src/modules/diagram/types/sequence/constants.test.ts
- apps/web/src/modules/diagram/types/sequence/SeqParticipantNode.tsx
- apps/web/src/modules/diagram/types/sequence/SeqFragmentNode.tsx
- apps/web/src/modules/diagram/types/sequence/SeqSyncEdge.tsx
- apps/web/src/modules/diagram/types/sequence/SeqAsyncEdge.tsx
- apps/web/src/modules/diagram/types/sequence/SeqReturnEdge.tsx
- apps/web/src/modules/diagram/lib/sequence-layout.ts
- apps/web/src/modules/diagram/lib/sequence-layout.test.ts

**Modified files:**
- apps/web/src/modules/diagram/lib/graph-converter.ts
- apps/web/src/modules/diagram/lib/graph-converter.test.ts
- apps/web/src/modules/diagram/lib/elk-layout.ts
- apps/web/src/modules/diagram/lib/elk-layout.test.ts
- apps/web/src/modules/diagram/hooks/useAutoLayout.ts
- apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx
- apps/web/src/assets/styles/globals.css

### Change Log

- 2026-02-27: Implemented Story 2.7 — Sequence Diagram Type Renderer. Added custom sequence layout, 2 node components, 3 edge components, constants, CSS styles, graph converter + canvas registration. 469 tests passing.
- 2026-02-27: Code review fixes (7 issues). CRITICAL: computeLayout now returns LayoutResult{nodes,edges?} so enriched sequence edges flow through useAutoLayout to the store — fixes all messages rendering at same Y. CRITICAL: computeSequenceLayout now computes activation bars (sync→open, return→close). MEDIUM: layout function no longer mutates input nodes/edges (immutable). MEDIUM: added sequence node size tests to elk-layout.test.ts. MEDIUM: edge components handle self-messages with U-shaped loop. LOW: seq-edge-label-positioned CSS class for position:absolute. LOW: corrected test count (469→476). 476 tests passing.
